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/AudioAndHapticFeedbackManager.java1
-rw-r--r--java/src/com/android/inputmethod/latin/BackupAgent.java30
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java216
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java13
-rw-r--r--java/src/com/android/inputmethod/latin/Constants.java311
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java30
-rw-r--r--java/src/com/android/inputmethod/latin/DicTraverseSession.java5
-rw-r--r--java/src/com/android/inputmethod/latin/Dictionary.java88
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryCollection.java36
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFacilitator.java638
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java157
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFactory.java6
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryStats.java43
-rw-r--r--java/src/com/android/inputmethod/latin/EmojiAltPhysicalKeyDetector.java93
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java366
-rw-r--r--java/src/com/android/inputmethod/latin/InputAttributes.java17
-rw-r--r--java/src/com/android/inputmethod/latin/InputPointers.java183
-rw-r--r--java/src/com/android/inputmethod/latin/InputView.java5
-rw-r--r--java/src/com/android/inputmethod/latin/LastComposedWord.java8
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java419
-rw-r--r--java/src/com/android/inputmethod/latin/NgramContext.java (renamed from java/src/com/android/inputmethod/latin/PrevWordsInfo.java)128
-rw-r--r--java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java185
-rw-r--r--java/src/com/android/inputmethod/latin/PunctuationSuggestions.java9
-rw-r--r--java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java16
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java79
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputMethodManager.java23
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java128
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeSwitcher.java99
-rw-r--r--java/src/com/android/inputmethod/latin/Suggest.java153
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestedWords.java63
-rw-r--r--java/src/com/android/inputmethod/latin/UserBinaryDictionary.java29
-rw-r--r--java/src/com/android/inputmethod/latin/WordComposer.java62
-rw-r--r--java/src/com/android/inputmethod/latin/accounts/AccountsChangedReceiver.java81
-rw-r--r--java/src/com/android/inputmethod/latin/accounts/AuthUtils.java67
-rw-r--r--java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java5
-rw-r--r--java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java333
-rw-r--r--java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java59
-rw-r--r--java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java40
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java10
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FormatSpec.java119
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/NgramProperty.java42
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java10
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/WordProperty.java108
-rw-r--r--java/src/com/android/inputmethod/latin/network/AuthException.java35
-rw-r--r--java/src/com/android/inputmethod/latin/network/BlockingHttpClient.java97
-rw-r--r--java/src/com/android/inputmethod/latin/network/HttpException.java46
-rw-r--r--java/src/com/android/inputmethod/latin/network/HttpUrlConnectionBuilder.java229
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/ContextualDictionary.java6
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java4
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java8
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java6
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java43
-rw-r--r--java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java266
-rw-r--r--java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java50
-rw-r--r--java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java7
-rw-r--r--java/src/com/android/inputmethod/latin/settings/CustomInputStylePreference.java359
-rw-r--r--java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java469
-rw-r--r--java/src/com/android/inputmethod/latin/settings/DebugSettings.java29
-rw-r--r--java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java82
-rw-r--r--java/src/com/android/inputmethod/latin/settings/GestureSettingsFragment.java1
-rw-r--r--java/src/com/android/inputmethod/latin/settings/LocalSettingsConstants.java61
-rw-r--r--java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java3
-rw-r--r--java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java9
-rw-r--r--java/src/com/android/inputmethod/latin/settings/RadioButtonPreference.java10
-rw-r--r--java/src/com/android/inputmethod/latin/settings/Settings.java33
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SettingsFragment.java6
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SettingsValues.java49
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java19
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java11
-rw-r--r--java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java55
-rw-r--r--java/src/com/android/inputmethod/latin/settings/ThemeSettingsFragment.java30
-rw-r--r--java/src/com/android/inputmethod/latin/setup/SetupActivity.java4
-rw-r--r--java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java9
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java141
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java26
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java38
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java30
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java6
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java2
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java10
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java170
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java34
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java2
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java12
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java25
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java13
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java6
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java35
-rw-r--r--java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java10
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java51
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CollectionUtils.java36
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java36
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java35
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java34
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java25
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DistracterFilter.java51
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java222
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java15
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java6
-rw-r--r--java/src/com/android/inputmethod/latin/utils/FragmentUtils.java2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java6
-rw-r--r--java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java187
-rw-r--r--java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java11
-rw-r--r--java/src/com/android/inputmethod/latin/utils/NgramContextUtils.java (renamed from java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java)33
-rw-r--r--java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java13
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java155
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ResourceUtils.java12
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java58
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java57
-rw-r--r--java/src/com/android/inputmethod/latin/utils/StringUtils.java631
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java77
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SuggestionResults.java15
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java4
-rw-r--r--java/src/com/android/inputmethod/latin/utils/WordInputEventForPersonalization.java118
114 files changed, 5177 insertions, 3822 deletions
diff --git a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
index eb8b34ccd..60d257362 100644
--- a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
+++ b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
@@ -22,6 +22,7 @@ import android.os.Vibrator;
import android.view.HapticFeedbackConstants;
import android.view.View;
+import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.settings.SettingsValues;
/**
diff --git a/java/src/com/android/inputmethod/latin/BackupAgent.java b/java/src/com/android/inputmethod/latin/BackupAgent.java
index 1f044618a..b2d92b30c 100644
--- a/java/src/com/android/inputmethod/latin/BackupAgent.java
+++ b/java/src/com/android/inputmethod/latin/BackupAgent.java
@@ -17,15 +17,41 @@
package com.android.inputmethod.latin;
import android.app.backup.BackupAgentHelper;
+import android.app.backup.BackupDataInput;
import android.app.backup.SharedPreferencesBackupHelper;
+import android.content.SharedPreferences;
+import android.os.ParcelFileDescriptor;
+
+import com.android.inputmethod.latin.settings.LocalSettingsConstants;
+
+import java.io.IOException;
/**
- * Backs up the Latin IME shared preferences.
+ * Backup/restore agent for LatinIME.
+ * Currently it backs up the default shared preferences.
*/
public final class BackupAgent extends BackupAgentHelper {
+ private static final String PREF_SUFFIX = "_preferences";
+
@Override
public void onCreate() {
addHelper("shared_pref", new SharedPreferencesBackupHelper(this,
- getPackageName() + "_preferences"));
+ getPackageName() + PREF_SUFFIX));
+ }
+
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+ throws IOException {
+ // Let the restore operation go through
+ super.onRestore(data, appVersionCode, newState);
+
+ // Remove the preferences that we don't want restored.
+ final SharedPreferences.Editor prefEditor = getSharedPreferences(
+ getPackageName() + PREF_SUFFIX, MODE_PRIVATE).edit();
+ for (final String key : LocalSettingsConstants.PREFS_TO_SKIP_RESTORING) {
+ prefEditor.remove(key);
+ }
+ // Flush the changes to disk.
+ prefEditor.commit();
}
}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 693e1cdcc..b5d0b446f 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -21,8 +21,11 @@ import android.util.Log;
import android.util.SparseArray;
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.InputPointers;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.makedict.DictionaryHeader;
import com.android.inputmethod.latin.makedict.FormatSpec;
import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions;
@@ -32,8 +35,7 @@ import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
import com.android.inputmethod.latin.utils.FileUtils;
import com.android.inputmethod.latin.utils.JniUtils;
-import com.android.inputmethod.latin.utils.LanguageModelParam;
-import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.utils.WordInputEventForPersonalization;
import java.io.File;
import java.util.ArrayList;
@@ -42,6 +44,8 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
+import javax.annotation.Nonnull;
+
/**
* Implements a static, compacted, binary dictionary of standard words.
*/
@@ -67,8 +71,8 @@ public final class BinaryDictionary extends Dictionary {
// Format to get unigram flags from native side via getWordPropertyNative().
private static final int FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT = 5;
private static final int FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX = 0;
- private static final int FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX = 1;
- private static final int FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX = 2;
+ private static final int FORMAT_WORD_PROPERTY_IS_POSSIBLY_OFFENSIVE_INDEX = 1;
+ private static final int FORMAT_WORD_PROPERTY_HAS_NGRAMS_INDEX = 2;
private static final int FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX = 3;
private static final int FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX = 4;
@@ -83,7 +87,6 @@ public final class BinaryDictionary extends Dictionary {
public static final String DIR_NAME_SUFFIX_FOR_RECORD_MIGRATION = ".migrating";
private long mNativeDict;
- private final Locale mLocale;
private final long mDictSize;
private final String mDictFilePath;
private final boolean mUseFullEditDistance;
@@ -117,8 +120,7 @@ public final class BinaryDictionary extends Dictionary {
public BinaryDictionary(final String filename, final long offset, final long length,
final boolean useFullEditDistance, final Locale locale, final String dictType,
final boolean isUpdatable) {
- super(dictType);
- mLocale = locale;
+ super(dictType, locale);
mDictSize = length;
mDictFilePath = filename;
mIsUpdatable = isUpdatable;
@@ -138,8 +140,7 @@ public final class BinaryDictionary extends Dictionary {
public BinaryDictionary(final String filename, final boolean useFullEditDistance,
final Locale locale, final String dictType, final long formatVersion,
final Map<String, String> attributeMap) {
- super(dictType);
- mLocale = locale;
+ super(dictType, locale);
mDictSize = 0;
mDictFilePath = filename;
// On memory dictionary is always updatable.
@@ -180,29 +181,34 @@ public final class BinaryDictionary extends Dictionary {
boolean[] isBeginningOfSentenceArray, int[] word);
private static native void getWordPropertyNative(long dict, int[] word,
boolean isBeginningOfSentence, int[] outCodePoints, boolean[] outFlags,
- int[] outProbabilityInfo, ArrayList<int[]> outBigramTargets,
- ArrayList<int[]> outBigramProbabilityInfo, ArrayList<int[]> outShortcutTargets,
- ArrayList<Integer> outShortcutProbabilities);
+ int[] outProbabilityInfo, ArrayList<int[][]> outNgramPrevWordsArray,
+ ArrayList<boolean[]> outNgramPrevWordIsBeginningOfSentenceArray,
+ ArrayList<int[]> outNgramTargets, ArrayList<int[]> outNgramProbabilityInfo,
+ ArrayList<int[]> outShortcutTargets, ArrayList<Integer> outShortcutProbabilities);
private static native int getNextWordNative(long dict, int token, int[] outCodePoints,
boolean[] outIsBeginningOfSentence);
private static native void getSuggestionsNative(long dict, long proximityInfo,
long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times,
int[] pointerIds, int[] inputCodePoints, int inputSize, int[] suggestOptions,
int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray,
- int[] outputSuggestionCount, int[] outputCodePoints, int[] outputScores,
- int[] outputIndices, int[] outputTypes, int[] outputAutoCommitFirstWordConfidence,
- float[] inOutLanguageWeight);
+ int prevWordCount, int[] outputSuggestionCount, int[] outputCodePoints,
+ int[] outputScores, int[] outputIndices, int[] outputTypes,
+ int[] outputAutoCommitFirstWordConfidence,
+ float[] inOutWeightOfLangModelVsSpatialModel);
private static native boolean addUnigramEntryNative(long dict, int[] word, int probability,
int[] shortcutTarget, int shortcutProbability, boolean isBeginningOfSentence,
- boolean isNotAWord, boolean isBlacklisted, int timestamp);
+ boolean isNotAWord, boolean isPossiblyOffensive, int timestamp);
private static native boolean removeUnigramEntryNative(long dict, int[] word);
private static native boolean addNgramEntryNative(long dict,
int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray,
int[] word, int probability, int timestamp);
private static native boolean removeNgramEntryNative(long dict,
int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray, int[] word);
- private static native int addMultipleDictionaryEntriesNative(long dict,
- LanguageModelParam[] languageModelParams, int startIndex);
+ private static native boolean updateEntriesForWordWithNgramContextNative(long dict,
+ int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray,
+ int[] word, boolean isValidWord, int count, int timestamp);
+ private static native int updateEntriesForInputEventsNative(long dict,
+ WordInputEventForPersonalization[] inputEvents, int startIndex);
private static native String getPropertyNative(long dict, String query);
private static native boolean isCorruptedNative(long dict);
private static native boolean migrateNative(long dict, String dictFilePath,
@@ -256,23 +262,25 @@ public final class BinaryDictionary extends Dictionary {
}
@Override
- public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
+ public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+ final NgramContext ngramContext, final long proximityInfoHandle,
final SettingsValuesForSuggestion settingsValuesForSuggestion,
- final int sessionId, final float[] inOutLanguageWeight) {
+ final int sessionId, final float weightForLocale,
+ final float[] inOutWeightOfLangModelVsSpatialModel) {
if (!isValidDictionary()) {
return null;
}
final DicTraverseSession session = getTraverseSession(sessionId);
Arrays.fill(session.mInputCodePoints, Constants.NOT_A_CODE);
- prevWordsInfo.outputToArray(session.mPrevWordCodePointArrays,
+ ngramContext.outputToArray(session.mPrevWordCodePointArrays,
session.mIsBeginningOfSentenceArray);
- final InputPointers inputPointers = composer.getInputPointers();
- final boolean isGesture = composer.isBatchMode();
+ final InputPointers inputPointers = composedData.mInputPointers;
+ final boolean isGesture = composedData.mIsBatchMode;
final int inputSize;
if (!isGesture) {
- inputSize = composer.copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
- session.mInputCodePoints);
+ inputSize =
+ composedData.copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
+ session.mInputCodePoints);
if (inputSize < 0) {
return null;
}
@@ -287,23 +295,28 @@ public final class BinaryDictionary extends Dictionary {
settingsValuesForSuggestion.mSpaceAwareGestureEnabled);
session.mNativeSuggestOptions.setAdditionalFeaturesOptions(
settingsValuesForSuggestion.mAdditionalFeaturesSettingValues);
- if (inOutLanguageWeight != null) {
- session.mInputOutputLanguageWeight[0] = inOutLanguageWeight[0];
+ session.mNativeSuggestOptions.setWeightForLocale(weightForLocale);
+ if (inOutWeightOfLangModelVsSpatialModel != null) {
+ session.mInputOutputWeightOfLangModelVsSpatialModel[0] =
+ inOutWeightOfLangModelVsSpatialModel[0];
} else {
- session.mInputOutputLanguageWeight[0] = Dictionary.NOT_A_LANGUAGE_WEIGHT;
+ session.mInputOutputWeightOfLangModelVsSpatialModel[0] =
+ Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL;
}
// TOOD: Pass multiple previous words information for n-gram.
- getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(),
+ getSuggestionsNative(mNativeDict, proximityInfoHandle,
getTraverseSession(sessionId).getSession(), inputPointers.getXCoordinates(),
inputPointers.getYCoordinates(), inputPointers.getTimes(),
inputPointers.getPointerIds(), session.mInputCodePoints, inputSize,
session.mNativeSuggestOptions.getOptions(), session.mPrevWordCodePointArrays,
- session.mIsBeginningOfSentenceArray, session.mOutputSuggestionCount,
- session.mOutputCodePoints, session.mOutputScores, session.mSpaceIndices,
- session.mOutputTypes, session.mOutputAutoCommitFirstWordConfidence,
- session.mInputOutputLanguageWeight);
- if (inOutLanguageWeight != null) {
- inOutLanguageWeight[0] = session.mInputOutputLanguageWeight[0];
+ session.mIsBeginningOfSentenceArray, ngramContext.getPrevWordCount(),
+ session.mOutputSuggestionCount, session.mOutputCodePoints, session.mOutputScores,
+ session.mSpaceIndices, session.mOutputTypes,
+ session.mOutputAutoCommitFirstWordConfidence,
+ session.mInputOutputWeightOfLangModelVsSpatialModel);
+ if (inOutWeightOfLangModelVsSpatialModel != null) {
+ inOutWeightOfLangModelVsSpatialModel[0] =
+ session.mInputOutputWeightOfLangModelVsSpatialModel[0];
}
final int count = session.mOutputSuggestionCount[0];
final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>();
@@ -317,7 +330,8 @@ public final class BinaryDictionary extends Dictionary {
if (len > 0) {
suggestions.add(new SuggestedWordInfo(
new String(session.mOutputCodePoints, start, len),
- session.mOutputScores[j], session.mOutputTypes[j], this /* sourceDict */,
+ (int)(session.mOutputScores[j] * weightForLocale), session.mOutputTypes[j],
+ this /* sourceDict */,
session.mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */,
session.mOutputAutoCommitFirstWordConfidence[0]));
}
@@ -340,31 +354,34 @@ public final class BinaryDictionary extends Dictionary {
@Override
public int getFrequency(final String word) {
- if (TextUtils.isEmpty(word)) return NOT_A_PROBABILITY;
- int[] codePoints = StringUtils.toCodePointArray(word);
+ if (TextUtils.isEmpty(word)) {
+ return NOT_A_PROBABILITY;
+ }
+ final int[] codePoints = StringUtils.toCodePointArray(word);
return getProbabilityNative(mNativeDict, codePoints);
}
@Override
public int getMaxFrequencyOfExactMatches(final String word) {
- if (TextUtils.isEmpty(word)) return NOT_A_PROBABILITY;
- int[] codePoints = StringUtils.toCodePointArray(word);
+ if (TextUtils.isEmpty(word)) {
+ return NOT_A_PROBABILITY;
+ }
+ final int[] codePoints = StringUtils.toCodePointArray(word);
return getMaxProbabilityOfExactMatchesNative(mNativeDict, codePoints);
}
@UsedForTesting
- public boolean isValidNgram(final PrevWordsInfo prevWordsInfo, final String word) {
- return getNgramProbability(prevWordsInfo, word) != NOT_A_PROBABILITY;
+ public boolean isValidNgram(final NgramContext ngramContext, final String word) {
+ return getNgramProbability(ngramContext, word) != NOT_A_PROBABILITY;
}
- public int getNgramProbability(final PrevWordsInfo prevWordsInfo, final String word) {
- if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) {
+ public int getNgramProbability(final NgramContext ngramContext, final String word) {
+ if (!ngramContext.isValid() || TextUtils.isEmpty(word)) {
return NOT_A_PROBABILITY;
}
- final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][];
- final boolean[] isBeginningOfSentenceArray =
- new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
- prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
+ final int[][] prevWordCodePointArrays = new int[ngramContext.getPrevWordCount()][];
+ final boolean[] isBeginningOfSentenceArray = new boolean[ngramContext.getPrevWordCount()];
+ ngramContext.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
final int[] wordCodePoints = StringUtils.toCodePointArray(word);
return getNgramProbabilityNative(mNativeDict, prevWordCodePointArrays,
isBeginningOfSentenceArray, wordCodePoints);
@@ -379,20 +396,25 @@ public final class BinaryDictionary extends Dictionary {
final boolean[] outFlags = new boolean[FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT];
final int[] outProbabilityInfo =
new int[FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT];
- final ArrayList<int[]> outBigramTargets = new ArrayList<>();
- final ArrayList<int[]> outBigramProbabilityInfo = new ArrayList<>();
+ final ArrayList<int[][]> outNgramPrevWordsArray = new ArrayList<>();
+ final ArrayList<boolean[]> outNgramPrevWordIsBeginningOfSentenceArray =
+ new ArrayList<>();
+ final ArrayList<int[]> outNgramTargets = new ArrayList<>();
+ final ArrayList<int[]> outNgramProbabilityInfo = new ArrayList<>();
final ArrayList<int[]> outShortcutTargets = new ArrayList<>();
final ArrayList<Integer> outShortcutProbabilities = new ArrayList<>();
getWordPropertyNative(mNativeDict, codePoints, isBeginningOfSentence, outCodePoints,
- outFlags, outProbabilityInfo, outBigramTargets, outBigramProbabilityInfo,
- outShortcutTargets, outShortcutProbabilities);
+ outFlags, outProbabilityInfo, outNgramPrevWordsArray,
+ outNgramPrevWordIsBeginningOfSentenceArray, outNgramTargets,
+ outNgramProbabilityInfo, outShortcutTargets, outShortcutProbabilities);
return new WordProperty(codePoints,
outFlags[FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX],
- outFlags[FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX],
- outFlags[FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX],
+ outFlags[FORMAT_WORD_PROPERTY_IS_POSSIBLY_OFFENSIVE_INDEX],
+ outFlags[FORMAT_WORD_PROPERTY_HAS_NGRAMS_INDEX],
outFlags[FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX],
outFlags[FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX], outProbabilityInfo,
- outBigramTargets, outBigramProbabilityInfo, outShortcutTargets,
+ outNgramPrevWordsArray, outNgramPrevWordIsBeginningOfSentenceArray,
+ outNgramTargets, outNgramProbabilityInfo, outShortcutTargets,
outShortcutProbabilities);
}
@@ -424,7 +446,7 @@ public final class BinaryDictionary extends Dictionary {
public boolean addUnigramEntry(final String word, final int probability,
final String shortcutTarget, final int shortcutProbability,
final boolean isBeginningOfSentence, final boolean isNotAWord,
- final boolean isBlacklisted, final int timestamp) {
+ final boolean isPossiblyOffensive, final int timestamp) {
if (word == null || (word.isEmpty() && !isBeginningOfSentence)) {
return false;
}
@@ -432,7 +454,8 @@ public final class BinaryDictionary extends Dictionary {
final int[] shortcutTargetCodePoints = (shortcutTarget != null) ?
StringUtils.toCodePointArray(shortcutTarget) : null;
if (!addUnigramEntryNative(mNativeDict, codePoints, probability, shortcutTargetCodePoints,
- shortcutProbability, isBeginningOfSentence, isNotAWord, isBlacklisted, timestamp)) {
+ shortcutProbability, isBeginningOfSentence, isNotAWord, isPossiblyOffensive,
+ timestamp)) {
return false;
}
mHasUpdated = true;
@@ -453,15 +476,14 @@ public final class BinaryDictionary extends Dictionary {
}
// Add an n-gram entry to the binary dictionary with timestamp in native code.
- public boolean addNgramEntry(final PrevWordsInfo prevWordsInfo, final String word,
+ public boolean addNgramEntry(final NgramContext ngramContext, final String word,
final int probability, final int timestamp) {
- if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) {
+ if (!ngramContext.isValid() || TextUtils.isEmpty(word)) {
return false;
}
- final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][];
- final boolean[] isBeginningOfSentenceArray =
- new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
- prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
+ final int[][] prevWordCodePointArrays = new int[ngramContext.getPrevWordCount()][];
+ final boolean[] isBeginningOfSentenceArray = new boolean[ngramContext.getPrevWordCount()];
+ ngramContext.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
final int[] wordCodePoints = StringUtils.toCodePointArray(word);
if (!addNgramEntryNative(mNativeDict, prevWordCodePointArrays,
isBeginningOfSentenceArray, wordCodePoints, probability, timestamp)) {
@@ -472,14 +494,13 @@ public final class BinaryDictionary extends Dictionary {
}
// Remove an n-gram entry from the binary dictionary in native code.
- public boolean removeNgramEntry(final PrevWordsInfo prevWordsInfo, final String word) {
- if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) {
+ public boolean removeNgramEntry(final NgramContext ngramContext, final String word) {
+ if (!ngramContext.isValid() || TextUtils.isEmpty(word)) {
return false;
}
- final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][];
- final boolean[] isBeginningOfSentenceArray =
- new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
- prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
+ final int[][] prevWordCodePointArrays = new int[ngramContext.getPrevWordCount()][];
+ final boolean[] isBeginningOfSentenceArray = new boolean[ngramContext.getPrevWordCount()];
+ ngramContext.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
final int[] wordCodePoints = StringUtils.toCodePointArray(word);
if (!removeNgramEntryNative(mNativeDict, prevWordCodePointArrays,
isBeginningOfSentenceArray, wordCodePoints)) {
@@ -489,17 +510,38 @@ public final class BinaryDictionary extends Dictionary {
return true;
}
- public void addMultipleDictionaryEntries(final LanguageModelParam[] languageModelParams) {
- if (!isValidDictionary()) return;
- int processedParamCount = 0;
- while (processedParamCount < languageModelParams.length) {
+ // Update entries for the word occurrence with the ngramContext.
+ public boolean updateEntriesForWordWithNgramContext(@Nonnull final NgramContext ngramContext,
+ final String word, final boolean isValidWord, final int count, final int timestamp) {
+ if (TextUtils.isEmpty(word)) {
+ return false;
+ }
+ final int[][] prevWordCodePointArrays = new int[ngramContext.getPrevWordCount()][];
+ final boolean[] isBeginningOfSentenceArray = new boolean[ngramContext.getPrevWordCount()];
+ ngramContext.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
+ final int[] wordCodePoints = StringUtils.toCodePointArray(word);
+ if (!updateEntriesForWordWithNgramContextNative(mNativeDict, prevWordCodePointArrays,
+ isBeginningOfSentenceArray, wordCodePoints, isValidWord, count, timestamp)) {
+ return false;
+ }
+ mHasUpdated = true;
+ return true;
+ }
+
+ @UsedForTesting
+ public void updateEntriesForInputEvents(final WordInputEventForPersonalization[] inputEvents) {
+ if (!isValidDictionary()) {
+ return;
+ }
+ int processedEventCount = 0;
+ while (processedEventCount < inputEvents.length) {
if (needsToRunGC(true /* mindsBlockByGC */)) {
flushWithGC();
}
- processedParamCount = addMultipleDictionaryEntriesNative(mNativeDict,
- languageModelParams, processedParamCount);
+ processedEventCount = updateEntriesForInputEventsNative(mNativeDict, inputEvents,
+ processedEventCount);
mHasUpdated = true;
- if (processedParamCount <= 0) {
+ if (processedEventCount <= 0) {
return;
}
}
@@ -517,7 +559,9 @@ public final class BinaryDictionary extends Dictionary {
// Flush to dict file if the dictionary has been updated.
public boolean flush() {
- if (!isValidDictionary()) return false;
+ if (!isValidDictionary()) {
+ return false;
+ }
if (mHasUpdated) {
if (!flushNative(mNativeDict, mDictFilePath)) {
return false;
@@ -537,7 +581,9 @@ public final class BinaryDictionary extends Dictionary {
// Run GC and flush to dict file.
public boolean flushWithGC() {
- if (!isValidDictionary()) return false;
+ if (!isValidDictionary()) {
+ return false;
+ }
if (!flushWithGCNative(mNativeDict, mDictFilePath)) {
return false;
}
@@ -552,7 +598,9 @@ public final class BinaryDictionary extends Dictionary {
* @return whether GC is needed to run or not.
*/
public boolean needsToRunGC(final boolean mindsBlockByGC) {
- if (!isValidDictionary()) return false;
+ if (!isValidDictionary()) {
+ return false;
+ }
return needsToRunGCNative(mNativeDict, mindsBlockByGC);
}
@@ -596,8 +644,10 @@ public final class BinaryDictionary extends Dictionary {
}
@UsedForTesting
- public String getPropertyForTest(final String query) {
- if (!isValidDictionary()) return "";
+ public String getPropertyForGettingStats(final String query) {
+ if (!isValidDictionary()) {
+ return "";
+ }
return getPropertyNative(mNativeDict, query);
}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 867c18686..1570bdae0 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -121,12 +121,11 @@ final public class BinaryDictionaryGetter {
// reason some dictionaries have been installed BUT the dictionary pack can't be
// found anymore it's safer to actually supply installed dictionaries.
return true;
- } else {
- // The default is true here for the same reasons as above. We got the dictionary
- // pack but if we don't have any settings for it it means the user has never been
- // to the settings yet. So by default, the main dictionaries should be on.
- return mDictPreferences.getBoolean(dictId, true);
}
+ // The default is true here for the same reasons as above. We got the dictionary
+ // pack but if we don't have any settings for it it means the user has never been
+ // to the settings yet. So by default, the main dictionaries should be on.
+ return mDictPreferences.getBoolean(dictId, true);
}
}
@@ -224,7 +223,7 @@ final public class BinaryDictionaryGetter {
// ## HACK ## we prevent usage of a dictionary before version 18. 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 file) {
+ private static boolean hackCanUseDictionaryFile(final File file) {
try {
// Read the version of the file
final DictionaryHeader header = BinaryDictionaryUtils.getHeader(file);
@@ -276,7 +275,7 @@ final public class BinaryDictionaryGetter {
// cachedWordLists may not be null, see doc for getCachedDictionaryList
for (final File f : cachedWordLists) {
final String wordListId = DictionaryInfoUtils.getWordListIdFromFileName(f.getName());
- final boolean canUse = f.canRead() && hackCanUseDictionaryFile(locale, f);
+ final boolean canUse = f.canRead() && hackCanUseDictionaryFile(f);
if (canUse && DictionaryInfoUtils.isMainWordListId(wordListId)) {
foundMainDict = true;
}
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
deleted file mode 100644
index 43af66eb7..000000000
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ /dev/null
@@ -1,311 +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;
-
-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
- * text field. For instance, this is specified by the search dialog when the dialog is
- * already showing a voice search button.
- *
- * @deprecated Use {@link ImeOption#NO_MICROPHONE} with package name prefixed.
- */
- @SuppressWarnings("dep-ann")
- public static final String NO_MICROPHONE_COMPAT = "nm";
-
- /**
- * The private IME option used to indicate that no microphone should be shown for a given
- * text field. For instance, this is specified by the search dialog when the dialog is
- * already showing a voice search button.
- */
- public static final String NO_MICROPHONE = "noMicrophoneKey";
-
- /**
- * The private IME option used to indicate that no settings key should be shown for a given
- * text field.
- */
- public static final String NO_SETTINGS_KEY = "noSettingsKey";
-
- /**
- * The private IME option used to indicate that the given text field needs ASCII code points
- * input.
- *
- * @deprecated Use EditorInfo#IME_FLAG_FORCE_ASCII.
- */
- @SuppressWarnings("dep-ann")
- public static final String FORCE_ASCII = "forceAscii";
-
- private ImeOption() {
- // This utility class is not publicly instantiable.
- }
- }
-
- public static final class Subtype {
- /**
- * The subtype mode used to indicate that the subtype is a keyboard.
- */
- public static final String KEYBOARD_MODE = "keyboard";
-
- public static final class ExtraValue {
- /**
- * The subtype extra value used to indicate that this subtype is capable of
- * entering ASCII characters.
- */
- public static final String ASCII_CAPABLE = "AsciiCapable";
-
- /**
- * The subtype extra value used to indicate that this subtype is enabled
- * when the default subtype is not marked as ascii capable.
- */
- public static final String ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
- "EnabledWhenDefaultIsNotAsciiCapable";
-
- /**
- * The subtype extra value used to indicate that this subtype is capable of
- * entering emoji characters.
- */
- public static final String EMOJI_CAPABLE = "EmojiCapable";
-
- /**
- * The subtype extra value used to indicate that this subtype requires a network
- * connection to work.
- */
- public static final String REQ_NETWORK_CONNECTIVITY = "requireNetworkConnectivity";
-
- /**
- * The subtype extra value used to indicate that the display name of this subtype
- * contains a "%s" for printf-like replacement and it should be replaced by
- * this extra value.
- * This extra value is supported on JellyBean and later.
- */
- public static final String UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME =
- "UntranslatableReplacementStringInSubtypeName";
-
- /**
- * The subtype extra value used to indicate this subtype keyboard layout set name.
- * This extra value is private to LatinIME.
- */
- public static final String KEYBOARD_LAYOUT_SET = "KeyboardLayoutSet";
-
- /**
- * The subtype extra value used to indicate that this subtype is an additional subtype
- * that the user defined. This extra value is private to LatinIME.
- */
- public static final String IS_ADDITIONAL_SUBTYPE = "isAdditionalSubtype";
-
- /**
- * The subtype extra value used to specify the combining rules.
- */
- public static final String COMBINING_RULES = "CombiningRules";
-
- private ExtraValue() {
- // This utility class is not publicly instantiable.
- }
- }
-
- private Subtype() {
- // This utility class is not publicly instantiable.
- }
- }
-
- public static final class TextUtils {
- /**
- * Capitalization mode for {@link android.text.TextUtils#getCapsMode}: don't capitalize
- * characters. This value may be used with
- * {@link android.text.TextUtils#CAP_MODE_CHARACTERS},
- * {@link android.text.TextUtils#CAP_MODE_WORDS}, and
- * {@link android.text.TextUtils#CAP_MODE_SENTENCES}.
- */
- // TODO: Straighten this out. It's bizarre to have to use android.text.TextUtils.CAP_MODE_*
- // except for OFF that is in Constants.TextUtils.
- public static final int CAP_MODE_OFF = 0;
-
- private TextUtils() {
- // This utility class is not publicly instantiable.
- }
- }
-
- public static final int NOT_A_CODE = -1;
- public static final int NOT_A_CURSOR_POSITION = -1;
- // TODO: replace the following constants with state in InputTransaction?
- public static final int NOT_A_COORDINATE = -1;
- public static final int SUGGESTION_STRIP_COORDINATE = -2;
- public static final int SPELL_CHECKER_COORDINATE = -3;
- public static final int EXTERNAL_KEYBOARD_COORDINATE = -4;
-
- // A hint on how many characters to cache from the TextView. A good value of this is given by
- // how many characters we need to be able to almost always find the caps mode.
- public static final int EDITOR_CONTENTS_CACHE_SIZE = 1024;
- // How many characters we accept for the recapitalization functionality. This needs to be
- // large enough for all reasonable purposes, but avoid purposeful attacks. 100k sounds about
- // right for this.
- public static final int MAX_CHARACTERS_FOR_RECAPITALIZATION = 1024 * 100;
-
- // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h
- public static final int DICTIONARY_MAX_WORD_LENGTH = 48;
-
- // (MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1)-gram is supported in Java side. Needs to modify
- // MAX_PREV_WORD_COUNT_FOR_N_GRAM in native/jni/src/defines.h for suggestions.
- public static final int MAX_PREV_WORD_COUNT_FOR_N_GRAM = 2;
-
- // Key events coming any faster than this are long-presses.
- public static final int LONG_PRESS_MILLISECONDS = 200;
- // TODO: Set this value appropriately.
- public static final int GET_SUGGESTED_WORDS_TIMEOUT = 200;
- // How many continuous deletes at which to start deleting at a higher speed.
- public static final int DELETE_ACCELERATE_AT = 20;
-
- public static final String WORD_SEPARATOR = " ";
-
- public static boolean isValidCoordinate(final int coordinate) {
- // Detect {@link NOT_A_COORDINATE}, {@link SUGGESTION_STRIP_COORDINATE},
- // and {@link SPELL_CHECKER_COORDINATE}.
- return coordinate >= 0;
- }
-
- /**
- * Custom request code used in
- * {@link com.android.inputmethod.keyboard.KeyboardActionListener#onCustomRequest(int)}.
- */
- // The code to show input method picker.
- public static final int CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER = 1;
-
- /**
- * Some common keys code. Must be positive.
- */
- public static final int CODE_ENTER = '\n';
- public static final int CODE_TAB = '\t';
- public static final int CODE_SPACE = ' ';
- public static final int CODE_PERIOD = '.';
- public static final int CODE_COMMA = ',';
- public static final int CODE_DASH = '-';
- public static final int CODE_SINGLE_QUOTE = '\'';
- public static final int CODE_DOUBLE_QUOTE = '"';
- public static final int CODE_QUESTION_MARK = '?';
- public static final int CODE_EXCLAMATION_MARK = '!';
- public static final int CODE_SLASH = '/';
- public static final int CODE_BACKSLASH = '\\';
- public static final int CODE_VERTICAL_BAR = '|';
- public static final int CODE_COMMERCIAL_AT = '@';
- public static final int CODE_PLUS = '+';
- public static final int CODE_PERCENT = '%';
- public static final int CODE_CLOSING_PARENTHESIS = ')';
- public static final int CODE_CLOSING_SQUARE_BRACKET = ']';
- public static final int CODE_CLOSING_CURLY_BRACKET = '}';
- public static final int CODE_CLOSING_ANGLE_BRACKET = '>';
- public static final int CODE_INVERTED_QUESTION_MARK = 0xBF; // ¿
- public static final int CODE_INVERTED_EXCLAMATION_MARK = 0xA1; // ¡
-
- public static final String REGEXP_PERIOD = "\\.";
- public static final String STRING_SPACE = " ";
- public static final String STRING_PERIOD_AND_SPACE = ". ";
-
- /**
- * Special keys code. Must be negative.
- * These should be aligned with {@link KeyboardCodesSet#ID_TO_NAME},
- * {@link KeyboardCodesSet#DEFAULT}, and {@link KeyboardCodesSet#RTL}.
- */
- public static final int CODE_SHIFT = -1;
- public static final int CODE_CAPSLOCK = -2;
- public static final int CODE_SWITCH_ALPHA_SYMBOL = -3;
- public static final int CODE_OUTPUT_TEXT = -4;
- public static final int CODE_DELETE = -5;
- public static final int CODE_SETTINGS = -6;
- public static final int CODE_SHORTCUT = -7;
- public static final int CODE_ACTION_NEXT = -8;
- public static final int CODE_ACTION_PREVIOUS = -9;
- public static final int CODE_LANGUAGE_SWITCH = -10;
- public static final int CODE_EMOJI = -11;
- public static final int CODE_SHIFT_ENTER = -12;
- public static final int CODE_SYMBOL_SHIFT = -13;
- public static final int CODE_ALPHA_FROM_EMOJI = -14;
- // Code value representing the code is not specified.
- public static final int CODE_UNSPECIFIED = -15;
-
- public static boolean isLetterCode(final int code) {
- return code >= CODE_SPACE;
- }
-
- public static String printableCode(final int code) {
- switch (code) {
- case CODE_SHIFT: return "shift";
- case CODE_CAPSLOCK: return "capslock";
- case CODE_SWITCH_ALPHA_SYMBOL: return "symbol";
- case CODE_OUTPUT_TEXT: return "text";
- case CODE_DELETE: return "delete";
- case CODE_SETTINGS: return "settings";
- case CODE_SHORTCUT: return "shortcut";
- case CODE_ACTION_NEXT: return "actionNext";
- case CODE_ACTION_PREVIOUS: return "actionPrevious";
- case CODE_LANGUAGE_SWITCH: return "languageSwitch";
- case CODE_EMOJI: return "emoji";
- case CODE_SHIFT_ENTER: return "shiftEnter";
- case CODE_ALPHA_FROM_EMOJI: return "alpha";
- case CODE_UNSPECIFIED: return "unspec";
- case CODE_TAB: return "tab";
- case CODE_ENTER: return "enter";
- case CODE_SPACE: return "space";
- default:
- if (code < CODE_SPACE) return String.format("\\u%02X", code);
- if (code < 0x100) return String.format("%c", code);
- if (code < 0x10000) return String.format("\\u%04X", code);
- return String.format("\\U%05X", code);
- }
- }
-
- public static String printableCodes(final int[] codes) {
- final StringBuilder sb = new StringBuilder();
- boolean addDelimiter = false;
- for (final int code : codes) {
- if (code == NOT_A_CODE) break;
- if (addDelimiter) sb.append(", ");
- sb.append(printableCode(code));
- addDelimiter = true;
- }
- return "[" + sb + "]";
- }
-
- public static final int MAX_INT_BIT_COUNT = 32;
-
- /**
- * Screen metrics (a.k.a. Device form factor) constants of
- * {@link R.integer#config_screen_metrics}.
- */
- public static final int SCREEN_METRICS_SMALL_PHONE = 0;
- public static final int SCREEN_METRICS_LARGE_PHONE = 1;
- public static final int SCREEN_METRICS_LARGE_TABLET = 2;
- public static final int SCREEN_METRICS_SMALL_TABLET = 3;
-
- /**
- * Default capacity of gesture points container.
- * This constant is used by {@link BatchInputArbiter} and etc. to preallocate regions that
- * contain gesture event points.
- */
- public static final int DEFAULT_GESTURE_POINTS_CAPACITY = 128;
-
- 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 162a209e3..59763c0fc 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -26,13 +26,13 @@ import android.os.SystemClock;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
-import android.text.TextUtils;
import android.util.Log;
-import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.annotations.ExternallyReferenced;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.personalization.AccountUtils;
import com.android.inputmethod.latin.utils.ExecutorUtils;
-import com.android.inputmethod.latin.utils.StringUtils;
import java.io.File;
import java.util.ArrayList;
@@ -83,7 +83,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
reloadDictionaryIfRequired();
}
- @UsedForTesting
+ // Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
+ @ExternallyReferenced
public static ContactsBinaryDictionary getDictionary(final Context context, final Locale locale,
final File dictFile, final String dictNamePrefix) {
return new ContactsBinaryDictionary(context, locale, dictFile, dictNamePrefix + NAME);
@@ -137,7 +138,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
addUnigramLocked(word, FREQUENCY_FOR_CONTACTS, null /* shortcut */,
- 0 /* shortcutFreq */, false /* isNotAWord */, false /* isBlacklisted */,
+ 0 /* shortcutFreq */, false /* isNotAWord */, false /* isPossiblyOffensive */,
BinaryDictionary.NOT_A_VALID_TIMESTAMP);
}
}
@@ -164,7 +165,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
}
}
- private boolean useFirstLastBigramsForLocale(final Locale locale) {
+ private static boolean useFirstLastBigramsForLocale(final Locale locale) {
// TODO: Add firstname/lastname bigram rules for other languages.
if (locale != null && locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
return true;
@@ -218,7 +219,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
*/
private void addNameLocked(final String name) {
int len = StringUtils.codePointCount(name);
- PrevWordsInfo prevWordsInfo = PrevWordsInfo.EMPTY_PREV_WORDS_INFO;
+ NgramContext ngramContext = NgramContext.EMPTY_PREV_WORDS_INFO;
// TODO: Better tokenization for non-Latin writing systems
for (int i = 0; i < len; i++) {
if (Character.isLetter(name.codePointAt(i))) {
@@ -233,19 +234,20 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
final int wordLen = StringUtils.codePointCount(word);
if (wordLen <= MAX_WORD_LENGTH && wordLen > 1) {
if (DEBUG) {
- Log.d(TAG, "addName " + name + ", " + word + ", " + prevWordsInfo);
+ Log.d(TAG, "addName " + name + ", " + word + ", " + ngramContext);
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
addUnigramLocked(word, FREQUENCY_FOR_CONTACTS,
null /* shortcut */, 0 /* shortcutFreq */, false /* isNotAWord */,
- false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
- if (!prevWordsInfo.isValid() && mUseFirstLastBigrams) {
+ false /* isPossiblyOffensive */,
+ BinaryDictionary.NOT_A_VALID_TIMESTAMP);
+ if (!ngramContext.isValid() && mUseFirstLastBigrams) {
runGCIfRequiredLocked(true /* mindsBlockByGC */);
- addNgramEntryLocked(prevWordsInfo, word, FREQUENCY_FOR_CONTACTS_BIGRAM,
+ addNgramEntryLocked(ngramContext, word, FREQUENCY_FOR_CONTACTS_BIGRAM,
BinaryDictionary.NOT_A_VALID_TIMESTAMP);
}
- prevWordsInfo = prevWordsInfo.getNextPrevWordsInfo(
- new PrevWordsInfo.WordInfo(word));
+ ngramContext = ngramContext.getNextNgramContext(
+ new NgramContext.WordInfo(word));
}
}
}
@@ -268,7 +270,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
return end;
}
- private boolean haveContentsChanged() {
+ boolean haveContentsChanged() {
final long startTime = SystemClock.uptimeMillis();
final int contactCount = getContactCount();
if (contactCount > MAX_CONTACT_COUNT) {
diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
index b341f623e..95390aa9f 100644
--- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java
+++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
@@ -16,6 +16,7 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.settings.NativeSuggestOptions;
import com.android.inputmethod.latin.utils.JniUtils;
@@ -40,7 +41,7 @@ public final class DicTraverseSession {
public final int[] mOutputTypes = new int[MAX_RESULTS];
// Only one result is ever used
public final int[] mOutputAutoCommitFirstWordConfidence = new int[1];
- public final float[] mInputOutputLanguageWeight = new float[1];
+ public final float[] mInputOutputWeightOfLangModelVsSpatialModel = new float[1];
public final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions();
@@ -70,7 +71,7 @@ public final class DicTraverseSession {
mNativeDicTraverseSession, dictionary, previousWord, previousWordLength);
}
- private final long createNativeDicTraverseSession(String locale, long dictSize) {
+ private static long createNativeDicTraverseSession(String locale, long dictSize) {
return setDicTraverseSessionNative(locale, dictSize);
}
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index 560ced9c4..7d7ed77e7 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -17,11 +17,14 @@
package com.android.inputmethod.latin;
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
import java.util.ArrayList;
+import java.util.Locale;
+import java.util.Arrays;
+import java.util.HashSet;
/**
* Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key
@@ -29,24 +32,24 @@ import java.util.ArrayList;
*/
public abstract class Dictionary {
public static final int NOT_A_PROBABILITY = -1;
- public static final float NOT_A_LANGUAGE_WEIGHT = -1.0f;
+ public static final float NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL = -1.0f;
// The following types do not actually come from real dictionary instances, so we create
// corresponding instances.
public static final String TYPE_USER_TYPED = "user_typed";
- public static final Dictionary DICTIONARY_USER_TYPED = new PhonyDictionary(TYPE_USER_TYPED);
+ public static final PhonyDictionary DICTIONARY_USER_TYPED = new PhonyDictionary(TYPE_USER_TYPED);
public static final String TYPE_APPLICATION_DEFINED = "application_defined";
- public static final Dictionary DICTIONARY_APPLICATION_DEFINED =
+ public static final PhonyDictionary DICTIONARY_APPLICATION_DEFINED =
new PhonyDictionary(TYPE_APPLICATION_DEFINED);
public static final String TYPE_HARDCODED = "hardcoded"; // punctuation signs and such
- public static final Dictionary DICTIONARY_HARDCODED =
+ public static final PhonyDictionary DICTIONARY_HARDCODED =
new PhonyDictionary(TYPE_HARDCODED);
// Spawned by resuming suggestions. Comes from a span that was in the TextView.
public static final String TYPE_RESUMED = "resumed";
- public static final Dictionary DICTIONARY_RESUMED =
+ public static final PhonyDictionary DICTIONARY_RESUMED =
new PhonyDictionary(TYPE_RESUMED);
// The following types of dictionary have actual functional instances. We don't need final
@@ -62,28 +65,45 @@ public abstract class Dictionary {
// Contextual dictionary.
public static final String TYPE_CONTEXTUAL = "contextual";
public final String mDictType;
+ // The locale for this dictionary. May be null if unknown (phony dictionary for example).
+ public final Locale mLocale;
- public Dictionary(final String dictType) {
+ /**
+ * Set out of the dictionary types listed above that are based on data specific to the user,
+ * e.g., the user's contacts.
+ */
+ private static final HashSet<String> sUserSpecificDictionaryTypes = new HashSet<>(Arrays.asList(
+ TYPE_USER_TYPED,
+ TYPE_USER,
+ TYPE_CONTACTS,
+ TYPE_USER_HISTORY,
+ TYPE_PERSONALIZATION,
+ TYPE_CONTEXTUAL));
+
+ public Dictionary(final String dictType, final Locale locale) {
mDictType = dictType;
+ mLocale = locale;
}
/**
- * 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 prevWordsInfo the information of previous words.
- * @param proximityInfo the object for key proximity. May be ignored by some implementations.
+ * Searches for suggestions for a given context.
+ * @param composedData the key sequence to match with coordinate info
+ * @param ngramContext the context for n-gram.
+ * @param proximityInfoHandle the handle for key proximity. Is ignored by some implementations.
* @param settingsValuesForSuggestion the settings values used for the suggestion.
* @param sessionId the session id.
- * @param inOutLanguageWeight the language weight used for generating suggestions.
- * inOutLanguageWeight is a float array that has only one element. This can be updated when the
- * different language weight is used.
+ * @param weightForLocale the weight given to this locale, to multiply the output scores for
+ * multilingual input.
+ * @param inOutWeightOfLangModelVsSpatialModel the weight of the language model as a ratio of
+ * the spatial model, used for generating suggestions. inOutWeightOfLangModelVsSpatialModel is
+ * a float array that has only one element. This can be updated when a different value is used.
* @return the list of suggestions (possibly null if none)
*/
- abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
+ abstract public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+ final NgramContext ngramContext, final long proximityInfoHandle,
final SettingsValuesForSuggestion settingsValuesForSuggestion,
- final int sessionId, final float[] inOutLanguageWeight);
+ final int sessionId, final float weightForLocale,
+ final float[] inOutWeightOfLangModelVsSpatialModel);
/**
* Checks if the given word has to be treated as a valid word. Please note that some
@@ -100,10 +120,18 @@ public abstract class Dictionary {
*/
abstract public boolean isInDictionary(final String word);
+ /**
+ * Get the frequency of the word.
+ * @param word the word to get the frequency of.
+ */
public int getFrequency(final String word) {
return NOT_A_PROBABILITY;
}
+ /**
+ * Get the maximum frequency of the word.
+ * @param word the word to get the maximum frequency of.
+ */
public int getMaxFrequencyOfExactMatches(final String word) {
return NOT_A_PROBABILITY;
}
@@ -156,20 +184,30 @@ public abstract class Dictionary {
}
/**
+ * Whether this dictionary is based on data specific to the user, e.g., the user's contacts.
+ * @return Whether this dictionary is specific to the user.
+ */
+ public boolean isUserSpecific() {
+ return sUserSpecificDictionaryTypes.contains(mDictType);
+ }
+
+ /**
* Not a true dictionary. A placeholder used to indicate suggestions that don't come from any
* real dictionary.
*/
- private static class PhonyDictionary extends Dictionary {
- // This class is not publicly instantiable.
- private PhonyDictionary(final String type) {
- super(type);
+ @UsedForTesting
+ static class PhonyDictionary extends Dictionary {
+ @UsedForTesting
+ PhonyDictionary(final String type) {
+ super(type, null);
}
@Override
- public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
+ public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+ final NgramContext ngramContext, final long proximityInfoHandle,
final SettingsValuesForSuggestion settingsValuesForSuggestion,
- final int sessionId, final float[] inOutLanguageWeight) {
+ final int sessionId, final float weightForLocale,
+ final float[] inOutWeightOfLangModelVsSpatialModel) {
return null;
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index 2b4c54d48..96575f629 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -18,13 +18,14 @@ package com.android.inputmethod.latin;
import android.util.Log;
-import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@@ -34,13 +35,14 @@ public final class DictionaryCollection extends Dictionary {
private final String TAG = DictionaryCollection.class.getSimpleName();
protected final CopyOnWriteArrayList<Dictionary> mDictionaries;
- public DictionaryCollection(final String dictType) {
- super(dictType);
+ public DictionaryCollection(final String dictType, final Locale locale) {
+ super(dictType, locale);
mDictionaries = new CopyOnWriteArrayList<>();
}
- public DictionaryCollection(final String dictType, final Dictionary... dictionaries) {
- super(dictType);
+ public DictionaryCollection(final String dictType, final Locale locale,
+ final Dictionary... dictionaries) {
+ super(dictType, locale);
if (null == dictionaries) {
mDictionaries = new CopyOnWriteArrayList<>();
} else {
@@ -49,30 +51,32 @@ public final class DictionaryCollection extends Dictionary {
}
}
- public DictionaryCollection(final String dictType, final Collection<Dictionary> dictionaries) {
- super(dictType);
+ public DictionaryCollection(final String dictType, final Locale locale,
+ final Collection<Dictionary> dictionaries) {
+ super(dictType, locale);
mDictionaries = new CopyOnWriteArrayList<>(dictionaries);
mDictionaries.removeAll(Collections.singleton(null));
}
@Override
- public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
+ public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+ final NgramContext ngramContext, final long proximityInfoHandle,
final SettingsValuesForSuggestion settingsValuesForSuggestion,
- final int sessionId, final float[] inOutLanguageWeight) {
+ final int sessionId, final float weightForLocale,
+ final float[] inOutWeightOfLangModelVsSpatialModel) {
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,
- prevWordsInfo, proximityInfo, settingsValuesForSuggestion, sessionId,
- inOutLanguageWeight);
+ ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composedData,
+ ngramContext, proximityInfoHandle, settingsValuesForSuggestion, sessionId,
+ weightForLocale, inOutWeightOfLangModelVsSpatialModel);
if (null == suggestions) suggestions = new ArrayList<>();
final int length = dictionaries.size();
for (int i = 1; i < length; ++ i) {
- final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer,
- prevWordsInfo, proximityInfo, settingsValuesForSuggestion, sessionId,
- inOutLanguageWeight);
+ final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(
+ composedData, ngramContext, proximityInfoHandle, settingsValuesForSuggestion,
+ sessionId, weightForLocale, inOutWeightOfLangModelVsSpatialModel);
if (null != sugg) suggestions.addAll(sugg);
}
return suggestions;
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
index fd1f51dd6..d23639a0d 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -19,12 +19,14 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.keyboard.ProximityInfo;
-import com.android.inputmethod.latin.PrevWordsInfo.WordInfo;
+import com.android.inputmethod.latin.ExpandableBinaryDictionary.UpdateEntriesForInputEventsCallback;
+import com.android.inputmethod.latin.NgramContext.WordInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.personalization.ContextualDictionary;
import com.android.inputmethod.latin.personalization.PersonalizationDataChunk;
import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
@@ -32,9 +34,9 @@ import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
import com.android.inputmethod.latin.utils.DistracterFilter;
+import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions;
import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary;
import com.android.inputmethod.latin.utils.ExecutorUtils;
-import com.android.inputmethod.latin.utils.LanguageModelParam;
import com.android.inputmethod.latin.utils.SuggestionResults;
import java.io.File;
@@ -51,6 +53,9 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
// TODO: Consolidate dictionaries in native code.
public class DictionaryFacilitator {
public static final String TAG = DictionaryFacilitator.class.getSimpleName();
@@ -59,12 +64,14 @@ public class DictionaryFacilitator {
// dictionary.
private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140;
- private Dictionaries mDictionaries = new Dictionaries();
+ private DictionaryGroup[] mDictionaryGroups = new DictionaryGroup[] { new DictionaryGroup() };
+ private DictionaryGroup mMostProbableDictionaryGroup = mDictionaryGroups[0];
private boolean mIsUserDictEnabled = false;
- private volatile CountDownLatch mLatchForWaitingLoadingMainDictionary = new CountDownLatch(0);
- // To synchronize assigning mDictionaries to ensure closing dictionaries.
+ private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0);
+ // To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
private final Object mLock = new Object();
private final DistracterFilter mDistracterFilter;
+ private final PersonalizationHelperForDictionaryFacilitator mPersonalizationHelper;
private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS =
new String[] {
@@ -96,22 +103,54 @@ public class DictionaryFacilitator {
DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS.length);
/**
- * Class contains dictionaries for a locale.
+ * Returns whether this facilitator is exactly for this list of locales.
+ * @param locales the list of locales to test against
+ * @return true if this facilitator handles exactly this list of locales, false otherwise
*/
- private static class Dictionaries {
+ public boolean isForLocales(final Locale[] locales) {
+ if (locales.length != mDictionaryGroups.length) {
+ return false;
+ }
+ for (final Locale locale : locales) {
+ boolean found = false;
+ for (final DictionaryGroup group : mDictionaryGroups) {
+ if (locale.equals(group.mLocale)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * A group of dictionaries that work together for a single language.
+ */
+ private static class DictionaryGroup {
+ // TODO: Run evaluation to determine a reasonable value for these constants. The current
+ // values are ad-hoc and chosen without any particular care or methodology.
+ public static final float WEIGHT_FOR_MOST_PROBABLE_LANGUAGE = 1.0f;
+ public static final float WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.95f;
+ public static final float WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.6f;
+
public final Locale mLocale;
private Dictionary mMainDict;
+ public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
+ public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap =
new ConcurrentHashMap<>();
- public Dictionaries() {
+ public DictionaryGroup() {
mLocale = null;
}
- public Dictionaries(final Locale locale, final Dictionary mainDict,
+ public DictionaryGroup(final Locale locale, final Dictionary mainDict,
final Map<String, ExpandableBinaryDictionary> subDicts) {
mLocale = locale;
- // Main dictionary can be asynchronously loaded.
+ // The main dictionary can be asynchronously loaded.
setMainDict(mainDict);
for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) {
setSubDict(entry.getKey(), entry.getValue());
@@ -136,9 +175,8 @@ public class DictionaryFacilitator {
public Dictionary getDict(final String dictType) {
if (Dictionary.TYPE_MAIN.equals(dictType)) {
return mMainDict;
- } else {
- return getSubDict(dictType);
}
+ return getSubDict(dictType);
}
public ExpandableBinaryDictionary getSubDict(final String dictType) {
@@ -148,9 +186,8 @@ public class DictionaryFacilitator {
public boolean hasDict(final String dictType) {
if (Dictionary.TYPE_MAIN.equals(dictType)) {
return mMainDict != null;
- } else {
- return mSubDictMap.containsKey(dictType);
}
+ return mSubDictMap.containsKey(dictType);
}
public void closeDict(final String dictType) {
@@ -172,20 +209,75 @@ public class DictionaryFacilitator {
public DictionaryFacilitator() {
mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER;
+ mPersonalizationHelper = null;
}
- public DictionaryFacilitator(final DistracterFilter distracterFilter) {
- mDistracterFilter = distracterFilter;
+ public DictionaryFacilitator(final Context context) {
+ mDistracterFilter = new DistracterFilterCheckingExactMatchesAndSuggestions(context);
+ mPersonalizationHelper =
+ new PersonalizationHelperForDictionaryFacilitator(context, mDistracterFilter);
}
public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) {
mDistracterFilter.updateEnabledSubtypes(enabledSubtypes);
+ mPersonalizationHelper.updateEnabledSubtypes(enabledSubtypes);
}
- public Locale getLocale() {
- return mDictionaries.mLocale;
+ // TODO: remove this, it's confusing with seamless multiple language switching
+ public void setIsMonolingualUser(final boolean isMonolingualUser) {
+ mPersonalizationHelper.setIsMonolingualUser(isMonolingualUser);
+ }
+
+ public boolean isActive() {
+ return null != mDictionaryGroups[0].mLocale;
+ }
+
+ /**
+ * Returns the most probable locale among all currently active locales. BE CAREFUL using this.
+ *
+ * DO NOT USE THIS just because it's convenient. Use it when it's correct, for example when
+ * choosing what dictionary to put a word in, or when changing the capitalization of a typed
+ * string.
+ * @return the most probable locale
+ */
+ public Locale getMostProbableLocale() {
+ return getDictionaryGroupForMostProbableLanguage().mLocale;
+ }
+
+ public Locale[] getLocales() {
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
+ final Locale[] locales = new Locale[dictionaryGroups.length];
+ for (int i = 0; i < dictionaryGroups.length; ++i) {
+ locales[i] = dictionaryGroups[i].mLocale;
+ }
+ return locales;
+ }
+
+ private DictionaryGroup getDictionaryGroupForMostProbableLanguage() {
+ return mMostProbableDictionaryGroup;
+ }
+
+ public void switchMostProbableLanguage(final Locale locale) {
+ if (null == locale) {
+ // In many cases, there is no locale to a committed word. For example, a typed word
+ // that does not auto-correct has no locale. In this case we simply do not change
+ // the most probable language.
+ return;
+ }
+ final DictionaryGroup newMostProbableDictionaryGroup =
+ findDictionaryGroupWithLocale(mDictionaryGroups, locale);
+ mMostProbableDictionaryGroup.mWeightForTypingInLocale =
+ DictionaryGroup.WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE;
+ mMostProbableDictionaryGroup.mWeightForGesturingInLocale =
+ DictionaryGroup.WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE;
+ newMostProbableDictionaryGroup.mWeightForTypingInLocale =
+ DictionaryGroup.WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
+ newMostProbableDictionaryGroup.mWeightForGesturingInLocale =
+ DictionaryGroup.WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
+ mMostProbableDictionaryGroup = newMostProbableDictionaryGroup;
}
+ @Nullable
private static ExpandableBinaryDictionary getSubDict(final String dictType,
final Context context, final Locale locale, final File dictFile,
final String dictNamePrefix) {
@@ -207,188 +299,288 @@ public class DictionaryFacilitator {
}
}
- public void resetDictionaries(final Context context, final Locale newLocale,
+ public void resetDictionaries(final Context context, final Locale[] newLocales,
final boolean useContactsDict, final boolean usePersonalizedDicts,
final boolean forceReloadMainDictionary,
final DictionaryInitializationListener listener) {
- resetDictionariesWithDictNamePrefix(context, newLocale, useContactsDict,
+ resetDictionariesWithDictNamePrefix(context, newLocales, useContactsDict,
usePersonalizedDicts, forceReloadMainDictionary, listener, "" /* dictNamePrefix */);
}
- public void resetDictionariesWithDictNamePrefix(final Context context, final Locale newLocale,
+ @Nullable
+ static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup[] dictionaryGroups,
+ final Locale locale) {
+ for (int i = 0; i < dictionaryGroups.length; ++i) {
+ if (locale.equals(dictionaryGroups[i].mLocale)) {
+ return dictionaryGroups[i];
+ }
+ }
+ return null;
+ }
+
+ public void resetDictionariesWithDictNamePrefix(final Context context,
+ final Locale[] newLocales,
final boolean useContactsDict, final boolean usePersonalizedDicts,
final boolean forceReloadMainDictionary,
- final DictionaryInitializationListener listener,
+ @Nullable final DictionaryInitializationListener listener,
final String dictNamePrefix) {
- final boolean localeHasBeenChanged = !newLocale.equals(mDictionaries.mLocale);
- // We always try to have the main dictionary. Other dictionaries can be unused.
- final boolean reloadMainDictionary = localeHasBeenChanged || forceReloadMainDictionary;
+ final HashMap<Locale, ArrayList<String>> existingDictsToCleanup = new HashMap<>();
// TODO: Make subDictTypesToUse configurable by resource or a static final list.
final HashSet<String> subDictTypesToUse = new HashSet<>();
+ subDictTypesToUse.add(Dictionary.TYPE_USER);
if (useContactsDict) {
subDictTypesToUse.add(Dictionary.TYPE_CONTACTS);
}
- subDictTypesToUse.add(Dictionary.TYPE_USER);
if (usePersonalizedDicts) {
subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY);
subDictTypesToUse.add(Dictionary.TYPE_PERSONALIZATION);
subDictTypesToUse.add(Dictionary.TYPE_CONTEXTUAL);
}
- final Dictionary newMainDict;
- if (reloadMainDictionary) {
- // The main dictionary will be asynchronously loaded.
- newMainDict = null;
- } else {
- newMainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN);
- }
-
- final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>();
- for (final String dictType : SUB_DICT_TYPES) {
- if (!subDictTypesToUse.contains(dictType)) {
- // This dictionary will not be used.
+ // Gather all dictionaries. We'll remove them from the list to clean up later.
+ for (final Locale newLocale : newLocales) {
+ final ArrayList<String> dictsForLocale = new ArrayList<>();
+ existingDictsToCleanup.put(newLocale, dictsForLocale);
+ final DictionaryGroup currentDictionaryGroupForLocale =
+ findDictionaryGroupWithLocale(mDictionaryGroups, newLocale);
+ if (null == currentDictionaryGroupForLocale) {
continue;
}
- final ExpandableBinaryDictionary dict;
- if (!localeHasBeenChanged && mDictionaries.hasDict(dictType)) {
- // Continue to use current dictionary.
- dict = mDictionaries.getSubDict(dictType);
+ for (final String dictType : SUB_DICT_TYPES) {
+ if (currentDictionaryGroupForLocale.hasDict(dictType)) {
+ dictsForLocale.add(dictType);
+ }
+ }
+ if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN)) {
+ dictsForLocale.add(Dictionary.TYPE_MAIN);
+ }
+ }
+
+ final DictionaryGroup[] newDictionaryGroups = new DictionaryGroup[newLocales.length];
+ for (int i = 0; i < newLocales.length; ++i) {
+ final Locale newLocale = newLocales[i];
+ final DictionaryGroup dictionaryGroupForLocale =
+ findDictionaryGroupWithLocale(mDictionaryGroups, newLocale);
+ final ArrayList<String> dictsToCleanupForLocale = existingDictsToCleanup.get(newLocale);
+ final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale);
+
+ final Dictionary mainDict;
+ if (forceReloadMainDictionary || noExistingDictsForThisLocale
+ || !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN)) {
+ mainDict = null;
} else {
- // Start to use new dictionary.
- dict = getSubDict(dictType, context, newLocale, null /* dictFile */,
- dictNamePrefix);
+ mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN);
+ dictsToCleanupForLocale.remove(Dictionary.TYPE_MAIN);
}
- subDicts.put(dictType, dict);
+
+ final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>();
+ for (final String subDictType : subDictTypesToUse) {
+ final ExpandableBinaryDictionary subDict;
+ if (noExistingDictsForThisLocale
+ || !dictionaryGroupForLocale.hasDict(subDictType)) {
+ // Create a new dictionary.
+ subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */,
+ dictNamePrefix);
+ } else {
+ // Reuse the existing dictionary, and don't close it at the end
+ subDict = dictionaryGroupForLocale.getSubDict(subDictType);
+ dictsToCleanupForLocale.remove(subDictType);
+ }
+ subDicts.put(subDictType, subDict);
+ }
+ newDictionaryGroups[i] = new DictionaryGroup(newLocale, mainDict, subDicts);
}
// Replace Dictionaries.
- final Dictionaries newDictionaries = new Dictionaries(newLocale, newMainDict, subDicts);
- final Dictionaries oldDictionaries;
+ final DictionaryGroup[] oldDictionaryGroups;
synchronized (mLock) {
- oldDictionaries = mDictionaries;
- mDictionaries = newDictionaries;
+ oldDictionaryGroups = mDictionaryGroups;
+ mDictionaryGroups = newDictionaryGroups;
+ mMostProbableDictionaryGroup = newDictionaryGroups[0];
mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context);
- if (reloadMainDictionary) {
- asyncReloadMainDictionary(context, newLocale, listener);
+ if (hasAtLeastOneUninitializedMainDictionary()) {
+ asyncReloadUninitializedMainDictionaries(context, newLocales, listener);
}
}
if (listener != null) {
- listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary());
+ listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary());
}
+
// Clean up old dictionaries.
- if (reloadMainDictionary) {
- oldDictionaries.closeDict(Dictionary.TYPE_MAIN);
- }
- for (final String dictType : SUB_DICT_TYPES) {
- if (localeHasBeenChanged || !subDictTypesToUse.contains(dictType)) {
- oldDictionaries.closeDict(dictType);
+ for (final Locale localeToCleanUp : existingDictsToCleanup.keySet()) {
+ final ArrayList<String> dictTypesToCleanUp =
+ existingDictsToCleanup.get(localeToCleanUp);
+ final DictionaryGroup dictionarySetToCleanup =
+ findDictionaryGroupWithLocale(oldDictionaryGroups, localeToCleanUp);
+ for (final String dictType : dictTypesToCleanUp) {
+ dictionarySetToCleanup.closeDict(dictType);
}
}
- oldDictionaries.mSubDictMap.clear();
}
- private void asyncReloadMainDictionary(final Context context, final Locale locale,
- final DictionaryInitializationListener listener) {
+ private void asyncReloadUninitializedMainDictionaries(final Context context,
+ final Locale[] locales, final DictionaryInitializationListener listener) {
final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1);
- mLatchForWaitingLoadingMainDictionary = latchForWaitingLoadingMainDictionary;
+ mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary;
ExecutorUtils.getExecutor("InitializeBinaryDictionary").execute(new Runnable() {
@Override
public void run() {
- final Dictionary mainDict =
- DictionaryFactory.createMainDictionaryFromManager(context, locale);
- synchronized (mLock) {
- if (locale.equals(mDictionaries.mLocale)) {
- mDictionaries.setMainDict(mainDict);
- } else {
- // Dictionary facilitator has been reset for another locale.
- mainDict.close();
- }
- }
- if (listener != null) {
- listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary());
- }
- latchForWaitingLoadingMainDictionary.countDown();
+ doReloadUninitializedMainDictionaries(
+ context, locales, listener, latchForWaitingLoadingMainDictionary);
}
});
}
+ void doReloadUninitializedMainDictionaries(final Context context, final Locale[] locales,
+ final DictionaryInitializationListener listener,
+ final CountDownLatch latchForWaitingLoadingMainDictionary) {
+ for (final Locale locale : locales) {
+ final DictionaryGroup dictionaryGroup =
+ findDictionaryGroupWithLocale(mDictionaryGroups, locale);
+ if (null == dictionaryGroup) {
+ // This should never happen, but better safe than crashy
+ Log.w(TAG, "Expected a dictionary group for " + locale + " but none found");
+ continue;
+ }
+ final Dictionary mainDict =
+ DictionaryFactory.createMainDictionaryFromManager(context, locale);
+ synchronized (mLock) {
+ if (locale.equals(dictionaryGroup.mLocale)) {
+ dictionaryGroup.setMainDict(mainDict);
+ } else {
+ // Dictionary facilitator has been reset for another locale.
+ mainDict.close();
+ }
+ }
+ }
+ if (listener != null) {
+ listener.onUpdateMainDictionaryAvailability(
+ hasAtLeastOneInitializedMainDictionary());
+ }
+ latchForWaitingLoadingMainDictionary.countDown();
+ }
+
@UsedForTesting
- public void resetDictionariesForTesting(final Context context, final Locale locale,
+ public void resetDictionariesForTesting(final Context context, final Locale[] locales,
final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles,
final Map<String, Map<String, String>> additionalDictAttributes) {
Dictionary mainDictionary = null;
final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>();
- for (final String dictType : dictionaryTypes) {
- if (dictType.equals(Dictionary.TYPE_MAIN)) {
- mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context, locale);
- } else {
- final File dictFile = dictionaryFiles.get(dictType);
- final ExpandableBinaryDictionary dict = getSubDict(
- dictType, context, locale, dictFile, "" /* dictNamePrefix */);
- if (additionalDictAttributes.containsKey(dictType)) {
- dict.clearAndFlushDictionaryWithAdditionalAttributes(
- additionalDictAttributes.get(dictType));
- }
- if (dict == null) {
- throw new RuntimeException("Unknown dictionary type: " + dictType);
+ final DictionaryGroup[] dictionaryGroups = new DictionaryGroup[locales.length];
+ for (int i = 0; i < locales.length; ++i) {
+ final Locale locale = locales[i];
+ for (final String dictType : dictionaryTypes) {
+ if (dictType.equals(Dictionary.TYPE_MAIN)) {
+ mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context,
+ locale);
+ } else {
+ final File dictFile = dictionaryFiles.get(dictType);
+ final ExpandableBinaryDictionary dict = getSubDict(
+ dictType, context, locale, dictFile, "" /* dictNamePrefix */);
+ if (additionalDictAttributes.containsKey(dictType)) {
+ dict.clearAndFlushDictionaryWithAdditionalAttributes(
+ additionalDictAttributes.get(dictType));
+ }
+ if (dict == null) {
+ throw new RuntimeException("Unknown dictionary type: " + dictType);
+ }
+ dict.reloadDictionaryIfRequired();
+ dict.waitAllTasksForTests();
+ subDicts.put(dictType, dict);
}
- dict.reloadDictionaryIfRequired();
- dict.waitAllTasksForTests();
- subDicts.put(dictType, dict);
}
+ dictionaryGroups[i] = new DictionaryGroup(locale, mainDictionary, subDicts);
}
- mDictionaries = new Dictionaries(locale, mainDictionary, subDicts);
+ mDictionaryGroups = dictionaryGroups;
+ mMostProbableDictionaryGroup = dictionaryGroups[0];
}
public void closeDictionaries() {
- final Dictionaries dictionaries;
+ final DictionaryGroup[] dictionaryGroups;
synchronized (mLock) {
- dictionaries = mDictionaries;
- mDictionaries = new Dictionaries();
+ dictionaryGroups = mDictionaryGroups;
+ mMostProbableDictionaryGroup = new DictionaryGroup();
+ mDictionaryGroups = new DictionaryGroup[] { mMostProbableDictionaryGroup };
}
- for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
- dictionaries.closeDict(dictType);
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
+ dictionaryGroup.closeDict(dictType);
+ }
}
mDistracterFilter.close();
+ if (mPersonalizationHelper != null) {
+ mPersonalizationHelper.close();
+ }
}
@UsedForTesting
public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) {
- return mDictionaries.getSubDict(dictName);
+ return mMostProbableDictionaryGroup.getSubDict(dictName);
}
- // The main dictionary could have been loaded asynchronously. Don't cache the return value
- // of this method.
- public boolean hasInitializedMainDictionary() {
- final Dictionary mainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN);
- return mainDict != null && mainDict.isInitialized();
+ // The main dictionaries are loaded asynchronously. Don't cache the return value
+ // of these methods.
+ public boolean hasAtLeastOneInitializedMainDictionary() {
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN);
+ if (mainDict != null && mainDict.isInitialized()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean hasAtLeastOneUninitializedMainDictionary() {
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN);
+ if (mainDict == null || !mainDict.isInitialized()) {
+ return true;
+ }
+ }
+ return false;
}
public boolean hasPersonalizationDictionary() {
- return mDictionaries.hasDict(Dictionary.TYPE_PERSONALIZATION);
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ if (dictionaryGroup.hasDict(Dictionary.TYPE_PERSONALIZATION)) {
+ return true;
+ }
+ }
+ return false;
}
public void flushPersonalizationDictionary() {
- final ExpandableBinaryDictionary personalizationDict =
- mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION);
- if (personalizationDict != null) {
- personalizationDict.asyncFlushBinaryDictionary();
- }
+ final HashSet<ExpandableBinaryDictionary> personalizationDictsUsedForSuggestion =
+ new HashSet<>();
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ final ExpandableBinaryDictionary personalizationDictUsedForSuggestion =
+ dictionaryGroup.getSubDict(Dictionary.TYPE_PERSONALIZATION);
+ personalizationDictsUsedForSuggestion.add(personalizationDictUsedForSuggestion);
+ }
+ mPersonalizationHelper.flushPersonalizationDictionariesToUpdate(
+ personalizationDictsUsedForSuggestion);
+ mDistracterFilter.close();
}
- public void waitForLoadingMainDictionary(final long timeout, final TimeUnit unit)
+ public void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit)
throws InterruptedException {
- mLatchForWaitingLoadingMainDictionary.await(timeout, unit);
+ mLatchForWaitingLoadingMainDictionaries.await(timeout, unit);
}
@UsedForTesting
public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit)
throws InterruptedException {
- waitForLoadingMainDictionary(timeout, unit);
- final Map<String, ExpandableBinaryDictionary> dictMap = mDictionaries.mSubDictMap;
- for (final ExpandableBinaryDictionary dict : dictMap.values()) {
- dict.waitAllTasksForTests();
+ waitForLoadingMainDictionaries(timeout, unit);
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ for (final ExpandableBinaryDictionary dict : dictionaryGroup.mSubDictMap.values()) {
+ dict.waitAllTasksForTests();
+ }
}
}
@@ -397,34 +589,35 @@ public class DictionaryFacilitator {
}
public void addWordToUserDictionary(final Context context, final String word) {
- final Locale locale = getLocale();
+ final Locale locale = getMostProbableLocale();
if (locale == null) {
return;
}
+ // TODO: add a toast telling what language this is being added to?
UserBinaryDictionary.addWordToUserDictionary(context, locale, word);
}
public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized,
- final PrevWordsInfo prevWordsInfo, final int timeStampInSeconds,
+ @Nonnull final NgramContext ngramContext, final int timeStampInSeconds,
final boolean blockPotentiallyOffensive) {
- final Dictionaries dictionaries = mDictionaries;
+ final DictionaryGroup dictionaryGroup = getDictionaryGroupForMostProbableLanguage();
final String[] words = suggestion.split(Constants.WORD_SEPARATOR);
- PrevWordsInfo prevWordsInfoForCurrentWord = prevWordsInfo;
+ NgramContext ngramContextForCurrentWord = ngramContext;
for (int i = 0; i < words.length; i++) {
final String currentWord = words[i];
final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false;
- addWordToUserHistory(dictionaries, prevWordsInfoForCurrentWord, currentWord,
+ addWordToUserHistory(dictionaryGroup, ngramContextForCurrentWord, currentWord,
wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive);
- prevWordsInfoForCurrentWord =
- prevWordsInfoForCurrentWord.getNextPrevWordsInfo(new WordInfo(currentWord));
+ ngramContextForCurrentWord =
+ ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord));
}
}
- private void addWordToUserHistory(final Dictionaries dictionaries,
- final PrevWordsInfo prevWordsInfo, final String word, final boolean wasAutoCapitalized,
+ private void addWordToUserHistory(final DictionaryGroup dictionaryGroup,
+ final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized,
final int timeStampInSeconds, final boolean blockPotentiallyOffensive) {
final ExpandableBinaryDictionary userHistoryDictionary =
- dictionaries.getSubDict(Dictionary.TYPE_USER_HISTORY);
+ dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY);
if (userHistoryDictionary == null) {
return;
}
@@ -432,7 +625,7 @@ public class DictionaryFacilitator {
if (maxFreq == 0 && blockPotentiallyOffensive) {
return;
}
- final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale);
+ final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale);
final String secondWord;
if (wasAutoCapitalized) {
if (isValidWord(word, false /* ignoreCase */)
@@ -453,8 +646,8 @@ public class DictionaryFacilitator {
// History dictionary in order to avoid suggesting them until the dictionary
// consolidation is done.
// TODO: Remove this hack when ready.
- final int lowerCaseFreqInMainDict = dictionaries.hasDict(Dictionary.TYPE_MAIN) ?
- dictionaries.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) :
+ final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN) ?
+ dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) :
Dictionary.NOT_A_PROBABILITY;
if (maxFreq < lowerCaseFreqInMainDict
&& lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) {
@@ -467,14 +660,15 @@ public class DictionaryFacilitator {
// We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
// We don't add words with 0-frequency (assuming they would be profanity etc.).
final boolean isValid = maxFreq > 0;
- UserHistoryDictionary.addToDictionary(userHistoryDictionary, prevWordsInfo, secondWord,
+ UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, secondWord,
isValid, timeStampInSeconds,
new DistracterFilterCheckingIsInDictionary(
mDistracterFilter, userHistoryDictionary));
}
private void removeWord(final String dictName, final String word) {
- final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName);
+ final ExpandableBinaryDictionary dictionary =
+ getDictionaryGroupForMostProbableLanguage().getSubDict(dictName);
if (dictionary != null) {
dictionary.removeUnigramEntryDynamically(word);
}
@@ -488,23 +682,29 @@ public class DictionaryFacilitator {
// TODO: Revise the way to fusion suggestion results.
public SuggestionResults getSuggestionResults(final WordComposer composer,
- final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
+ final NgramContext ngramContext, final long proximityInfoHandle,
final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) {
- final Dictionaries dictionaries = mDictionaries;
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
final SuggestionResults suggestionResults = new SuggestionResults(
- dictionaries.mLocale, SuggestedWords.MAX_SUGGESTIONS,
- prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence);
- final float[] languageWeight = new float[] { Dictionary.NOT_A_LANGUAGE_WEIGHT };
- for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
- final Dictionary dictionary = dictionaries.getDict(dictType);
- if (null == dictionary) continue;
- final ArrayList<SuggestedWordInfo> dictionarySuggestions =
- dictionary.getSuggestions(composer, prevWordsInfo, proximityInfo,
- settingsValuesForSuggestion, sessionId, languageWeight);
- if (null == dictionarySuggestions) continue;
- suggestionResults.addAll(dictionarySuggestions);
- if (null != suggestionResults.mRawSuggestions) {
- suggestionResults.mRawSuggestions.addAll(dictionarySuggestions);
+ SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext());
+ final float[] weightOfLangModelVsSpatialModel =
+ new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL };
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
+ final Dictionary dictionary = dictionaryGroup.getDict(dictType);
+ if (null == dictionary) continue;
+ final float weightForLocale = composer.isBatchMode()
+ ? dictionaryGroup.mWeightForGesturingInLocale
+ : dictionaryGroup.mWeightForTypingInLocale;
+ final ArrayList<SuggestedWordInfo> dictionarySuggestions =
+ dictionary.getSuggestions(composer.getComposedDataSnapshot(), ngramContext,
+ proximityInfoHandle, settingsValuesForSuggestion, sessionId,
+ weightForLocale, weightOfLangModelVsSpatialModel);
+ if (null == dictionarySuggestions) continue;
+ suggestionResults.addAll(dictionarySuggestions);
+ if (null != suggestionResults.mRawSuggestions) {
+ suggestionResults.mRawSuggestions.addAll(dictionarySuggestions);
+ }
}
}
return suggestionResults;
@@ -514,20 +714,22 @@ public class DictionaryFacilitator {
if (TextUtils.isEmpty(word)) {
return false;
}
- final Dictionaries dictionaries = mDictionaries;
- if (dictionaries.mLocale == null) {
- return false;
- }
- final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale);
- for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
- final Dictionary dictionary = dictionaries.getDict(dictType);
- // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
- // would be immutable once it's finished initializing, but concretely a null test is
- // probably good enough for the time being.
- if (null == dictionary) continue;
- if (dictionary.isValidWord(word)
- || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) {
- return true;
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ if (dictionaryGroup.mLocale == null) {
+ continue;
+ }
+ final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale);
+ for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
+ final Dictionary dictionary = dictionaryGroup.getDict(dictType);
+ // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
+ // would be immutable once it's finished initializing, but concretely a null test is
+ // probably good enough for the time being.
+ if (null == dictionary) continue;
+ if (dictionary.isValidWord(word)
+ || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) {
+ return true;
+ }
}
}
return false;
@@ -539,18 +741,20 @@ public class DictionaryFacilitator {
return Dictionary.NOT_A_PROBABILITY;
}
int maxFreq = Dictionary.NOT_A_PROBABILITY;
- final Dictionaries dictionaries = mDictionaries;
- for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
- final Dictionary dictionary = dictionaries.getDict(dictType);
- if (dictionary == null) continue;
- final int tempFreq;
- if (isGettingMaxFrequencyOfExactMatches) {
- tempFreq = dictionary.getMaxFrequencyOfExactMatches(word);
- } else {
- tempFreq = dictionary.getFrequency(word);
- }
- if (tempFreq >= maxFreq) {
- maxFreq = tempFreq;
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
+ final Dictionary dictionary = dictionaryGroup.getDict(dictType);
+ if (dictionary == null) continue;
+ final int tempFreq;
+ if (isGettingMaxFrequencyOfExactMatches) {
+ tempFreq = dictionary.getMaxFrequencyOfExactMatches(word);
+ } else {
+ tempFreq = dictionary.getFrequency(word);
+ }
+ if (tempFreq >= maxFreq) {
+ maxFreq = tempFreq;
+ }
}
}
return maxFreq;
@@ -565,9 +769,12 @@ public class DictionaryFacilitator {
}
private void clearSubDictionary(final String dictName) {
- final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName);
- if (dictionary != null) {
- dictionary.clear();
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictName);
+ if (dictionary != null) {
+ dictionary.clear();
+ }
}
}
@@ -579,6 +786,7 @@ public class DictionaryFacilitator {
// personalization dictionary.
public void clearPersonalizationDictionary() {
clearSubDictionary(Dictionary.TYPE_PERSONALIZATION);
+ mPersonalizationHelper.clearDictionariesToUpdate();
}
public void clearContextualDictionary() {
@@ -588,73 +796,73 @@ public class DictionaryFacilitator {
public void addEntriesToPersonalizationDictionary(
final PersonalizationDataChunk personalizationDataChunk,
final SpacingAndPunctuations spacingAndPunctuations,
- final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) {
- final ExpandableBinaryDictionary personalizationDict =
- mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION);
- if (personalizationDict == null) {
- if (callback != null) {
- callback.onFinished();
- }
- return;
- }
- final ArrayList<LanguageModelParam> languageModelParams =
- LanguageModelParam.createLanguageModelParamsFrom(
- personalizationDataChunk.mTokens,
- personalizationDataChunk.mTimestampInSeconds,
- this /* dictionaryFacilitator */, spacingAndPunctuations,
- new DistracterFilterCheckingIsInDictionary(
- mDistracterFilter, personalizationDict));
- if (languageModelParams == null || languageModelParams.isEmpty()) {
- if (callback != null) {
- callback.onFinished();
- }
- return;
- }
- personalizationDict.addMultipleDictionaryEntriesDynamically(languageModelParams, callback);
+ final UpdateEntriesForInputEventsCallback callback) {
+ mPersonalizationHelper.updateEntriesOfPersonalizationDictionaries(
+ getMostProbableLocale(), personalizationDataChunk, spacingAndPunctuations,
+ callback);
}
+ @UsedForTesting
public void addPhraseToContextualDictionary(final String[] phrase, final int probability,
final int bigramProbabilityForWords, final int bigramProbabilityForPhrases) {
+ // TODO: we're inserting the phrase into the dictionary for the active language. Rethink
+ // this a bit from a theoretical point of view.
final ExpandableBinaryDictionary contextualDict =
- mDictionaries.getSubDict(Dictionary.TYPE_CONTEXTUAL);
+ getDictionaryGroupForMostProbableLanguage().getSubDict(Dictionary.TYPE_CONTEXTUAL);
if (contextualDict == null) {
return;
}
- PrevWordsInfo prevWordsInfo = PrevWordsInfo.BEGINNING_OF_SENTENCE;
+ NgramContext ngramContext = NgramContext.BEGINNING_OF_SENTENCE;
for (int i = 0; i < phrase.length; i++) {
final String[] subPhrase = Arrays.copyOfRange(phrase, i /* start */, phrase.length);
final String subPhraseStr = TextUtils.join(Constants.WORD_SEPARATOR, subPhrase);
contextualDict.addUnigramEntryWithCheckingDistracter(
subPhraseStr, probability, null /* shortcutTarget */,
Dictionary.NOT_A_PROBABILITY /* shortcutFreq */,
- false /* isNotAWord */, false /* isBlacklisted */,
+ false /* isNotAWord */, false /* isPossiblyOffensive */,
BinaryDictionary.NOT_A_VALID_TIMESTAMP,
DistracterFilter.EMPTY_DISTRACTER_FILTER);
- contextualDict.addNgramEntry(prevWordsInfo, subPhraseStr,
+ contextualDict.addNgramEntry(ngramContext, subPhraseStr,
bigramProbabilityForPhrases, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
if (i < phrase.length - 1) {
contextualDict.addUnigramEntryWithCheckingDistracter(
phrase[i], probability, null /* shortcutTarget */,
Dictionary.NOT_A_PROBABILITY /* shortcutFreq */,
- false /* isNotAWord */, false /* isBlacklisted */,
+ false /* isNotAWord */, false /* isPossiblyOffensive */,
BinaryDictionary.NOT_A_VALID_TIMESTAMP,
DistracterFilter.EMPTY_DISTRACTER_FILTER);
- contextualDict.addNgramEntry(prevWordsInfo, phrase[i],
+ contextualDict.addNgramEntry(ngramContext, phrase[i],
bigramProbabilityForWords, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
}
- prevWordsInfo =
- prevWordsInfo.getNextPrevWordsInfo(new PrevWordsInfo.WordInfo(phrase[i]));
+ ngramContext =
+ ngramContext.getNextNgramContext(new NgramContext.WordInfo(phrase[i]));
}
}
public void dumpDictionaryForDebug(final String dictName) {
- final ExpandableBinaryDictionary dictToDump = mDictionaries.getSubDict(dictName);
- if (dictToDump == null) {
- Log.e(TAG, "Cannot dump " + dictName + ". "
- + "The dictionary is not being used for suggestion or cannot be dumped.");
- return;
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ final ExpandableBinaryDictionary dictToDump = dictionaryGroup.getSubDict(dictName);
+ if (dictToDump == null) {
+ Log.e(TAG, "Cannot dump " + dictName + ". "
+ + "The dictionary is not being used for suggestion or cannot be dumped.");
+ return;
+ }
+ dictToDump.dumpAllWordsForDebug();
+ }
+ }
+
+ public ArrayList<Pair<String, DictionaryStats>> getStatsOfEnabledSubDicts() {
+ final ArrayList<Pair<String, DictionaryStats>> statsOfEnabledSubDicts = new ArrayList<>();
+ final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
+ for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ for (final String dictType : SUB_DICT_TYPES) {
+ final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictType);
+ if (dictionary == null) continue;
+ statsOfEnabledSubDicts.add(new Pair<>(dictType, dictionary.getDictionaryStats()));
+ }
}
- dictToDump.dumpAllWordsForDebug();
+ return statsOfEnabledSubDicts;
}
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java
new file mode 100644
index 000000000..b578159eb
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2014 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.HashSet;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import android.content.Context;
+import android.util.Log;
+import android.util.LruCache;
+
+/**
+ * Cache for dictionary facilitators of multiple locales.
+ * This class automatically creates and releases facilitator instances using LRU policy.
+ */
+public class DictionaryFacilitatorLruCache {
+ static final String TAG = DictionaryFacilitatorLruCache.class.getSimpleName();
+ private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000;
+ private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5;
+
+ /**
+ * Class extends LruCache. This class tracks cached locales and closes evicted dictionaries by
+ * overriding entryRemoved.
+ */
+ private static class DictionaryFacilitatorLruCacheInner extends
+ LruCache<Locale, DictionaryFacilitator> {
+ private final HashSet<Locale> mCachedLocales;
+ public DictionaryFacilitatorLruCacheInner(final HashSet<Locale> cachedLocales,
+ final int maxSize) {
+ super(maxSize);
+ mCachedLocales = cachedLocales;
+ }
+
+ @Override
+ protected void entryRemoved(boolean evicted, Locale key,
+ DictionaryFacilitator oldValue, DictionaryFacilitator newValue) {
+ if (oldValue != null && oldValue != newValue) {
+ oldValue.closeDictionaries();
+ }
+ if (key != null && newValue == null) {
+ // Remove locale from the cache when the dictionary facilitator for the locale is
+ // evicted and new facilitator is not set for the locale.
+ mCachedLocales.remove(key);
+ if (size() >= maxSize()) {
+ Log.w(TAG, "DictionaryFacilitator for " + key.toString()
+ + " has been evicted due to cache size limit."
+ + " size: " + size() + ", maxSize: " + maxSize());
+ }
+ }
+ }
+ }
+
+ private final Context mContext;
+ private final HashSet<Locale> mCachedLocales = new HashSet<>();
+ private final String mDictionaryNamePrefix;
+ private final DictionaryFacilitatorLruCacheInner mLruCache;
+ private final Object mLock = new Object();
+ private boolean mUseContactsDictionary = false;
+
+ public DictionaryFacilitatorLruCache(final Context context, final int maxSize,
+ final String dictionaryNamePrefix) {
+ mContext = context;
+ mLruCache = new DictionaryFacilitatorLruCacheInner(mCachedLocales, maxSize);
+ mDictionaryNamePrefix = dictionaryNamePrefix;
+ }
+
+ private static void waitForLoadingMainDictionary(
+ final DictionaryFacilitator dictionaryFacilitator) {
+ for (int i = 0; i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT; i++) {
+ try {
+ dictionaryFacilitator.waitForLoadingMainDictionaries(
+ WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
+ return;
+ } catch (final InterruptedException e) {
+ Log.i(TAG, "Interrupted during waiting for loading main dictionary.", e);
+ if (i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT - 1) {
+ Log.i(TAG, "Retry", e);
+ } else {
+ Log.w(TAG, "Give up retrying. Retried "
+ + MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT + " times.", e);
+ }
+ }
+ }
+ }
+
+ private void resetDictionariesForLocaleLocked(final DictionaryFacilitator dictionaryFacilitator,
+ final Locale locale) {
+ dictionaryFacilitator.resetDictionariesWithDictNamePrefix(mContext, new Locale[] { locale },
+ mUseContactsDictionary, false /* usePersonalizedDicts */,
+ false /* forceReloadMainDictionary */, null /* listener */,
+ mDictionaryNamePrefix);
+ }
+
+ public void setUseContactsDictionary(final boolean useContectsDictionary) {
+ if (mUseContactsDictionary == useContectsDictionary) {
+ // The value has not been changed.
+ return;
+ }
+ synchronized (mLock) {
+ mUseContactsDictionary = useContectsDictionary;
+ for (final Locale locale : mCachedLocales) {
+ final DictionaryFacilitator dictionaryFacilitator = mLruCache.get(locale);
+ resetDictionariesForLocaleLocked(dictionaryFacilitator, locale);
+ waitForLoadingMainDictionary(dictionaryFacilitator);
+ }
+ }
+ }
+
+ public DictionaryFacilitator get(final Locale locale) {
+ DictionaryFacilitator dictionaryFacilitator = mLruCache.get(locale);
+ if (dictionaryFacilitator != null) {
+ // dictionary falicitator for the locale is in the cache.
+ return dictionaryFacilitator;
+ }
+ synchronized (mLock) {
+ dictionaryFacilitator = mLruCache.get(locale);
+ if (dictionaryFacilitator != null) {
+ return dictionaryFacilitator;
+ }
+ dictionaryFacilitator = new DictionaryFacilitator();
+ resetDictionariesForLocaleLocked(dictionaryFacilitator, locale);
+ waitForLoadingMainDictionary(dictionaryFacilitator);
+ mLruCache.put(locale, dictionaryFacilitator);
+ mCachedLocales.add(locale);
+ return dictionaryFacilitator;
+ }
+ }
+
+ public void evictAll() {
+ synchronized (mLock) {
+ mLruCache.evictAll();
+ mCachedLocales.clear();
+ }
+ }
+
+ @UsedForTesting
+ HashSet<Locale> getCachedLocalesForTesting() {
+ return mCachedLocales;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index 59de4f82a..3459b426d 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -50,7 +50,7 @@ public final class DictionaryFactory {
final Locale locale, final boolean useFullEditDistance) {
if (null == locale) {
Log.e(TAG, "No locale defined for dictionary");
- return new DictionaryCollection(Dictionary.TYPE_MAIN,
+ return new DictionaryCollection(Dictionary.TYPE_MAIN, locale,
createReadOnlyBinaryDictionary(context, locale));
}
@@ -75,7 +75,7 @@ public final 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(Dictionary.TYPE_MAIN, dictList);
+ return new DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList);
}
/**
@@ -188,7 +188,7 @@ public final class DictionaryFactory {
public static Dictionary createDictionaryForTest(final AssetFileAddress[] dictionaryList,
final boolean useFullEditDistance, Locale locale) {
final DictionaryCollection dictionaryCollection =
- new DictionaryCollection(Dictionary.TYPE_MAIN);
+ new DictionaryCollection(Dictionary.TYPE_MAIN, locale);
for (final AssetFileAddress address : dictionaryList) {
final ReadOnlyBinaryDictionary readOnlyBinaryDictionary = new ReadOnlyBinaryDictionary(
address.mFilename, address.mOffset, address.mLength, useFullEditDistance,
diff --git a/java/src/com/android/inputmethod/latin/DictionaryStats.java b/java/src/com/android/inputmethod/latin/DictionaryStats.java
new file mode 100644
index 000000000..5dd39d3d6
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryStats.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014 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.io.File;
+import java.util.Locale;
+
+public class DictionaryStats {
+ public static final int NOT_AN_ENTRY_COUNT = -1;
+
+ public final Locale mLocale;
+ public final String mDictName;
+ public final String mDictFilePath;
+ public final long mDictFileSize;
+
+ public final int mUnigramCount;
+ public final int mNgramCount;
+ // TODO: Add more members.
+
+ public DictionaryStats(final Locale locale, final String dictName, final File dictFile,
+ final int unigramCount, final int ngramCount) {
+ mLocale = locale;
+ mDictName = dictName;
+ mDictFilePath = dictFile.getAbsolutePath();
+ mDictFileSize = dictFile.length();
+ mUnigramCount = unigramCount;
+ mNgramCount = ngramCount;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/EmojiAltPhysicalKeyDetector.java b/java/src/com/android/inputmethod/latin/EmojiAltPhysicalKeyDetector.java
new file mode 100644
index 000000000..8116a4983
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/EmojiAltPhysicalKeyDetector.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014, 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.KeyEvent;
+
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.latin.settings.Settings;
+
+/**
+ * A class for detecting Emoji-Alt physical key.
+ */
+final class EmojiAltPhysicalKeyDetector {
+ // True if the Alt key has been used as a modifier. In this case the Alt key up isn't
+ // recognized as an emoji key.
+ private boolean mAltHasBeenUsedAsAModifier;
+
+ /**
+ * Record a down key event.
+ * @param keyEvent a down key event.
+ */
+ public void onKeyDown(final KeyEvent keyEvent) {
+ if (isAltKey(keyEvent)) {
+ mAltHasBeenUsedAsAModifier = false;
+ }
+ if (containsAltModifier(keyEvent)) {
+ mAltHasBeenUsedAsAModifier = true;
+ }
+ }
+
+ /**
+ * Determine whether an up key event is a special key up or not.
+ * @param keyEvent an up key event.
+ */
+ public void onKeyUp(final KeyEvent keyEvent) {
+ if (keyEvent.isCanceled()) {
+ // This key up event was a part of key combinations and should be ignored.
+ return;
+ }
+ if (!isAltKey(keyEvent)) {
+ mAltHasBeenUsedAsAModifier |= containsAltModifier(keyEvent);
+ return;
+ }
+ if (containsAltModifier(keyEvent)) {
+ mAltHasBeenUsedAsAModifier = true;
+ return;
+ }
+ if (!Settings.getInstance().getCurrent().mEnableEmojiAltPhysicalKey) {
+ return;
+ }
+ if (!mAltHasBeenUsedAsAModifier) {
+ onEmojiAltKeyDetected();
+ }
+ }
+
+ private static void onEmojiAltKeyDetected() {
+ KeyboardSwitcher.getInstance().onToggleEmojiKeyboard();
+ }
+
+ private static boolean isAltKey(final KeyEvent keyEvent) {
+ final int keyCode = keyEvent.getKeyCode();
+ return keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT;
+ }
+
+ private static boolean containsAltModifier(final KeyEvent keyEvent) {
+ final int metaState = keyEvent.getMetaState();
+ // TODO: Support multiple keyboards. Take device id into account.
+ switch (keyEvent.getKeyCode()) {
+ case KeyEvent.KEYCODE_ALT_LEFT:
+ // Return true if Left-Alt is pressed with Right-Alt pressed.
+ return (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
+ case KeyEvent.KEYCODE_ALT_RIGHT:
+ // Return true if Right-Alt is pressed with Left-Alt pressed.
+ return (metaState & KeyEvent.META_ALT_LEFT_ON) != 0;
+ default:
+ return (metaState & (KeyEvent.META_ALT_LEFT_ON | KeyEvent.META_ALT_RIGHT_ON)) != 0;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index c11a220a4..b47eaa9bb 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -20,18 +20,20 @@ import android.content.Context;
import android.util.Log;
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
+import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.makedict.DictionaryHeader;
import com.android.inputmethod.latin.makedict.FormatSpec;
import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
import com.android.inputmethod.latin.makedict.WordProperty;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
+import com.android.inputmethod.latin.utils.AsyncResultHolder;
import com.android.inputmethod.latin.utils.CombinedFormatUtils;
import com.android.inputmethod.latin.utils.DistracterFilter;
import com.android.inputmethod.latin.utils.ExecutorUtils;
import com.android.inputmethod.latin.utils.FileUtils;
-import com.android.inputmethod.latin.utils.LanguageModelParam;
+import com.android.inputmethod.latin.utils.WordInputEventForPersonalization;
import java.io.File;
import java.util.ArrayList;
@@ -45,10 +47,17 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
/**
* Abstract base class for an expandable dictionary that can be created and updated dynamically
* during runtime. When updated it automatically generates a new binary dictionary to handle future
* queries in native code. This binary dictionary is written to internal storage.
+ *
+ * A class that extends this abstract class must have a static factory method named
+ * getDictionary(Context context, Locale locale, File dictFile, String dictNamePrefix)
+ * @see DictionaryFacilitator#getSubDict(String,Context,Locale,File,String)
*/
abstract public class ExpandableBinaryDictionary extends Dictionary {
private static final boolean DEBUG = false;
@@ -61,9 +70,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
private static final int TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS = 100;
- private static final int DEFAULT_MAX_UNIGRAM_COUNT = 10000;
- private static final int DEFAULT_MAX_BIGRAM_COUNT = 10000;
-
/**
* The maximum length of a word in this dictionary.
*/
@@ -86,9 +92,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
private final String mDictName;
- /** Dictionary locale */
- private final Locale mLocale;
-
/** Dictionary file */
private final File mDictFile;
@@ -110,14 +113,15 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
protected abstract void loadInitialContentsLocked();
- private boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) {
+ static boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) {
return formatVersion == FormatSpec.VERSION4;
}
- private boolean needsToMigrateDictionary(final int formatVersion) {
+ private static boolean needsToMigrateDictionary(final int formatVersion) {
// When we bump up the dictionary format version, the old version should be added to here
// for supporting migration. Note that native code has to support reading such formats.
- return formatVersion == FormatSpec.VERSION4_ONLY_FOR_TESTING;
+ return formatVersion == FormatSpec.VERSION4_ONLY_FOR_TESTING
+ || formatVersion == FormatSpec.VERSION402;
}
public boolean isValidDictionaryLocked() {
@@ -137,10 +141,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
public ExpandableBinaryDictionary(final Context context, final String dictName,
final Locale locale, final String dictType, final File dictFile) {
- super(dictType);
+ super(dictType, locale);
mDictName = dictName;
mContext = context;
- mLocale = locale;
mDictFile = getDictFile(context, dictName, dictFile);
mBinaryDictionary = null;
mIsReloading = new AtomicBoolean();
@@ -160,23 +163,26 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
private void asyncExecuteTaskWithWriteLock(final Runnable task) {
- asyncExecuteTaskWithLock(mLock.writeLock(), task);
+ asyncExecuteTaskWithLock(mLock.writeLock(), mDictName /* executorName */, task);
}
- private void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) {
- asyncPreCheckAndExecuteTaskWithLock(lock, null /* preCheckTask */, task);
+ private static void asyncExecuteTaskWithLock(final Lock lock, final String executorName,
+ final Runnable task) {
+ asyncPreCheckAndExecuteTaskWithLock(lock, null /* preCheckTask */, executorName, task);
}
private void asyncPreCheckAndExecuteTaskWithWriteLock(
final Callable<Boolean> preCheckTask, final Runnable task) {
- asyncPreCheckAndExecuteTaskWithLock(mLock.writeLock(), preCheckTask, task);
+ asyncPreCheckAndExecuteTaskWithLock(mLock.writeLock(), preCheckTask,
+ mDictName /* executorName */, task);
}
// Execute task with lock when the result of preCheckTask is true or preCheckTask is null.
- private void asyncPreCheckAndExecuteTaskWithLock(final Lock lock,
- final Callable<Boolean> preCheckTask, final Runnable task) {
- ExecutorUtils.getExecutor(mDictName).execute(new Runnable() {
+ private static void asyncPreCheckAndExecuteTaskWithLock(final Lock lock,
+ final Callable<Boolean> preCheckTask, final String executorName, final Runnable task) {
+ final String tag = TAG;
+ ExecutorUtils.getExecutor(executorName).execute(new Runnable() {
@Override
public void run() {
if (preCheckTask != null) {
@@ -185,7 +191,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
return;
}
} catch (final Exception e) {
- Log.e(TAG, "The pre check task throws an exception.", e);
+ Log.e(tag, "The pre check task throws an exception.", e);
return;
}
}
@@ -199,6 +205,18 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
});
}
+ @Nullable
+ BinaryDictionary getBinaryDictionary() {
+ return mBinaryDictionary;
+ }
+
+ void closeBinaryDictionary() {
+ if (mBinaryDictionary != null) {
+ mBinaryDictionary.close();
+ mBinaryDictionary = null;
+ }
+ }
+
/**
* Closes and cleans up the binary dictionary.
*/
@@ -207,10 +225,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
- if (mBinaryDictionary != null) {
- mBinaryDictionary.close();
- mBinaryDictionary = null;
- }
+ closeBinaryDictionary();
}
});
}
@@ -224,10 +239,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
attributeMap.put(DictionaryHeader.DICTIONARY_LOCALE_KEY, mLocale.toString());
attributeMap.put(DictionaryHeader.DICTIONARY_VERSION_KEY,
String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
- attributeMap.put(DictionaryHeader.MAX_UNIGRAM_COUNT_KEY,
- String.valueOf(DEFAULT_MAX_UNIGRAM_COUNT));
- attributeMap.put(DictionaryHeader.MAX_BIGRAM_COUNT_KEY,
- String.valueOf(DEFAULT_MAX_BIGRAM_COUNT));
return attributeMap;
}
@@ -240,14 +251,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
});
}
- private void removeBinaryDictionaryLocked() {
- if (mBinaryDictionary != null) {
- mBinaryDictionary.close();
- }
+ void removeBinaryDictionaryLocked() {
+ closeBinaryDictionary();
if (mDictFile.exists() && !FileUtils.deleteRecursively(mDictFile)) {
Log.e(TAG, "Can't remove a file: " + mDictFile.getName());
}
- mBinaryDictionary = null;
}
private void openBinaryDictionaryLocked() {
@@ -256,7 +264,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
true /* useFullEditDistance */, mLocale, mDictType, true /* isUpdatable */);
}
- private void createOnMemoryBinaryDictionaryLocked() {
+ void createOnMemoryBinaryDictionaryLocked() {
mBinaryDictionary = new BinaryDictionary(
mDictFile.getAbsolutePath(), true /* useFullEditDistance */, mLocale, mDictType,
DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
@@ -279,7 +287,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
- if (mBinaryDictionary == null) {
+ if (getBinaryDictionary() == null) {
return;
}
runGCIfRequiredLocked(mindsBlockByGC);
@@ -293,40 +301,51 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
}
+ private void updateDictionaryWithWriteLockIfWordIsNotADistracter(
+ @Nonnull final Runnable updateTask,
+ @Nonnull final String word, @Nonnull final DistracterFilter distracterFilter) {
+ reloadDictionaryIfRequired();
+ final Callable<Boolean> preCheckTask = new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return !distracterFilter.isDistracterToWordsInDictionaries(
+ NgramContext.EMPTY_PREV_WORDS_INFO, word, mLocale);
+ }
+ };
+ final Runnable task = new Runnable() {
+ @Override
+ public void run() {
+ if (getBinaryDictionary() == null) {
+ return;
+ }
+ runGCIfRequiredLocked(true /* mindsBlockByGC */);
+ updateTask.run();
+ }
+ };
+ asyncPreCheckAndExecuteTaskWithWriteLock(preCheckTask, task);
+ }
+
/**
* Adds unigram information of a word to the dictionary. May overwrite an existing entry.
*/
public void addUnigramEntryWithCheckingDistracter(final String word, final int frequency,
final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord,
- final boolean isBlacklisted, final int timestamp,
- final DistracterFilter distracterFilter) {
- reloadDictionaryIfRequired();
- asyncPreCheckAndExecuteTaskWithWriteLock(
- new Callable<Boolean>() {
- @Override
- public Boolean call() throws Exception {
- return !distracterFilter.isDistracterToWordsInDictionaries(
- PrevWordsInfo.EMPTY_PREV_WORDS_INFO, word, mLocale);
- }
- },
- new Runnable() {
- @Override
- public void run() {
- if (mBinaryDictionary == null) {
- return;
- }
- runGCIfRequiredLocked(true /* mindsBlockByGC */);
- addUnigramLocked(word, frequency, shortcutTarget, shortcutFreq,
- isNotAWord, isBlacklisted, timestamp);
- }
- });
+ final boolean isPossiblyOffensive, final int timestamp,
+ @Nonnull final DistracterFilter distracterFilter) {
+ updateDictionaryWithWriteLockIfWordIsNotADistracter(new Runnable() {
+ @Override
+ public void run() {
+ addUnigramLocked(word, frequency, shortcutTarget, shortcutFreq,
+ isNotAWord, isPossiblyOffensive, timestamp);
+ }
+ }, word, distracterFilter);
}
protected void addUnigramLocked(final String word, final int frequency,
final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord,
- final boolean isBlacklisted, final int timestamp) {
+ final boolean isPossiblyOffensive, final int timestamp) {
if (!mBinaryDictionary.addUnigramEntry(word, frequency, shortcutTarget, shortcutFreq,
- false /* isBeginningOfSentence */, isNotAWord, isBlacklisted, timestamp)) {
+ false /* isBeginningOfSentence */, isNotAWord, isPossiblyOffensive, timestamp)) {
Log.e(TAG, "Cannot add unigram entry. word: " + word);
}
}
@@ -339,11 +358,12 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
- if (mBinaryDictionary == null) {
+ final BinaryDictionary binaryDictionary = getBinaryDictionary();
+ if (binaryDictionary == null) {
return;
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
- if (!mBinaryDictionary.removeUnigramEntry(word)) {
+ if (!binaryDictionary.removeUnigramEntry(word)) {
if (DEBUG) {
Log.i(TAG, "Cannot remove unigram entry: " + word);
}
@@ -355,27 +375,27 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
/**
* Adds n-gram information of a word to the dictionary. May overwrite an existing entry.
*/
- public void addNgramEntry(final PrevWordsInfo prevWordsInfo, final String word,
+ public void addNgramEntry(@Nonnull final NgramContext ngramContext, final String word,
final int frequency, final int timestamp) {
reloadDictionaryIfRequired();
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
- if (mBinaryDictionary == null) {
+ if (getBinaryDictionary() == null) {
return;
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
- addNgramEntryLocked(prevWordsInfo, word, frequency, timestamp);
+ addNgramEntryLocked(ngramContext, word, frequency, timestamp);
}
});
}
- protected void addNgramEntryLocked(final PrevWordsInfo prevWordsInfo, final String word,
+ protected void addNgramEntryLocked(@Nonnull final NgramContext ngramContext, final String word,
final int frequency, final int timestamp) {
- if (!mBinaryDictionary.addNgramEntry(prevWordsInfo, word, frequency, timestamp)) {
+ if (!mBinaryDictionary.addNgramEntry(ngramContext, word, frequency, timestamp)) {
if (DEBUG) {
Log.i(TAG, "Cannot add n-gram entry.");
- Log.i(TAG, " PrevWordsInfo: " + prevWordsInfo + ", word: " + word);
+ Log.i(TAG, " NgramContext: " + ngramContext + ", word: " + word);
}
}
}
@@ -384,46 +404,73 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* Dynamically remove the n-gram entry in the dictionary.
*/
@UsedForTesting
- public void removeNgramDynamically(final PrevWordsInfo prevWordsInfo, final String word) {
+ public void removeNgramDynamically(@Nonnull final NgramContext ngramContext,
+ final String word) {
reloadDictionaryIfRequired();
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
- if (mBinaryDictionary == null) {
+ final BinaryDictionary binaryDictionary = getBinaryDictionary();
+ if (binaryDictionary == null) {
return;
}
runGCIfRequiredLocked(true /* mindsBlockByGC */);
- if (!mBinaryDictionary.removeNgramEntry(prevWordsInfo, word)) {
+ if (!binaryDictionary.removeNgramEntry(ngramContext, word)) {
if (DEBUG) {
Log.i(TAG, "Cannot remove n-gram entry.");
- Log.i(TAG, " PrevWordsInfo: " + prevWordsInfo + ", word: " + word);
+ Log.i(TAG, " NgramContext: " + ngramContext + ", word: " + word);
}
}
}
});
}
- public interface AddMultipleDictionaryEntriesCallback {
+ /**
+ * Update dictionary for the word with the ngramContext if the word is not a distracter.
+ */
+ public void updateEntriesForWordWithCheckingDistracter(@Nonnull final NgramContext ngramContext,
+ final String word, final boolean isValidWord, final int count, final int timestamp,
+ @Nonnull final DistracterFilter distracterFilter) {
+ updateDictionaryWithWriteLockIfWordIsNotADistracter(new Runnable() {
+ @Override
+ public void run() {
+ final BinaryDictionary binaryDictionary = getBinaryDictionary();
+ if (binaryDictionary == null) {
+ return;
+ }
+ if (!binaryDictionary.updateEntriesForWordWithNgramContext(ngramContext, word,
+ isValidWord, count, timestamp)) {
+ if (DEBUG) {
+ Log.e(TAG, "Cannot update counter. word: " + word
+ + " context: "+ ngramContext.toString());
+ }
+ }
+ }
+ }, word, distracterFilter);
+ }
+
+ public interface UpdateEntriesForInputEventsCallback {
public void onFinished();
}
/**
- * Dynamically add multiple entries to the dictionary.
+ * Dynamically update entries according to input events.
*/
- public void addMultipleDictionaryEntriesDynamically(
- final ArrayList<LanguageModelParam> languageModelParams,
- final AddMultipleDictionaryEntriesCallback callback) {
+ public void updateEntriesForInputEvents(
+ @Nonnull final ArrayList<WordInputEventForPersonalization> inputEvents,
+ final UpdateEntriesForInputEventsCallback callback) {
reloadDictionaryIfRequired();
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
try {
- if (mBinaryDictionary == null) {
+ final BinaryDictionary binaryDictionary = getBinaryDictionary();
+ if (binaryDictionary == null) {
return;
}
- mBinaryDictionary.addMultipleDictionaryEntries(
- languageModelParams.toArray(
- new LanguageModelParam[languageModelParams.size()]));
+ binaryDictionary.updateEntriesForInputEvents(
+ inputEvents.toArray(
+ new WordInputEventForPersonalization[inputEvents.size()]));
} finally {
if (callback != null) {
callback.onFinished();
@@ -434,10 +481,10 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
@Override
- public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
+ public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+ final NgramContext ngramContext, final long proximityInfoHandle,
final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId,
- final float[] inOutLanguageWeight) {
+ final float weightForLocale, final float[] inOutWeightOfLangModelVsSpatialModel) {
reloadDictionaryIfRequired();
boolean lockAcquired = false;
try {
@@ -448,8 +495,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
return null;
}
final ArrayList<SuggestedWordInfo> suggestions =
- mBinaryDictionary.getSuggestions(composer, prevWordsInfo, proximityInfo,
- settingsValuesForSuggestion, sessionId, inOutLanguageWeight);
+ mBinaryDictionary.getSuggestions(composedData, ngramContext,
+ proximityInfoHandle, settingsValuesForSuggestion, sessionId,
+ weightForLocale, inOutWeightOfLangModelVsSpatialModel);
if (mBinaryDictionary.isCorrupted()) {
Log.i(TAG, "Dictionary (" + mDictName +") is corrupted. "
+ "Remove and regenerate it.");
@@ -519,16 +567,16 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
- protected boolean isValidNgramLocked(final PrevWordsInfo prevWordsInfo, final String word) {
+ protected boolean isValidNgramLocked(final NgramContext ngramContext, final String word) {
if (mBinaryDictionary == null) return false;
- return mBinaryDictionary.isValidNgram(prevWordsInfo, word);
+ return mBinaryDictionary.isValidNgram(ngramContext, word);
}
/**
* Loads the current binary dictionary from internal storage. Assumes the dictionary file
* exists.
*/
- private void loadBinaryDictionaryLocked() {
+ void loadBinaryDictionaryLocked() {
if (DBG_STRESS_TEST) {
// Test if this class does not cause problems when it takes long time to load binary
// dictionary.
@@ -556,7 +604,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
/**
* Create a new binary dictionary and load initial contents.
*/
- private void createNewDictionaryLocked() {
+ void createNewDictionaryLocked() {
removeBinaryDictionaryLocked();
createOnMemoryBinaryDictionaryLocked();
loadInitialContentsLocked();
@@ -572,6 +620,14 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
mNeedsToRecreate = true;
}
+ void clearNeedsToRecreate() {
+ mNeedsToRecreate = false;
+ }
+
+ boolean isNeededToRecreate() {
+ return mNeedsToRecreate;
+ }
+
/**
* Load the current binary dictionary from internal storage. If the dictionary file doesn't
* exists or needs to be regenerated, the new dictionary file will be asynchronously generated.
@@ -594,35 +650,39 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* Reloads the dictionary. Access is controlled on a per dictionary file basis.
*/
private final void asyncReloadDictionary() {
- if (mIsReloading.compareAndSet(false, true)) {
- asyncExecuteTaskWithWriteLock(new Runnable() {
- @Override
- public void run() {
- try {
- if (!mDictFile.exists() || mNeedsToRecreate) {
- // If the dictionary file does not exist or contents have been updated,
- // generate a new one.
+ final AtomicBoolean isReloading = mIsReloading;
+ if (!isReloading.compareAndSet(false, true)) {
+ return;
+ }
+ final File dictFile = mDictFile;
+ asyncExecuteTaskWithWriteLock(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (!dictFile.exists() || isNeededToRecreate()) {
+ // If the dictionary file does not exist or contents have been updated,
+ // generate a new one.
+ createNewDictionaryLocked();
+ } else if (getBinaryDictionary() == null) {
+ // Otherwise, load the existing dictionary.
+ loadBinaryDictionaryLocked();
+ final BinaryDictionary binaryDictionary = getBinaryDictionary();
+ if (binaryDictionary != null && !(isValidDictionaryLocked()
+ // TODO: remove the check below
+ && matchesExpectedBinaryDictFormatVersionForThisType(
+ binaryDictionary.getFormatVersion()))) {
+ // Binary dictionary or its format version is not valid. Regenerate
+ // the dictionary file. createNewDictionaryLocked will remove the
+ // existing files if appropriate.
createNewDictionaryLocked();
- } else if (mBinaryDictionary == null) {
- // Otherwise, load the existing dictionary.
- loadBinaryDictionaryLocked();
- if (mBinaryDictionary != null && !(isValidDictionaryLocked()
- // TODO: remove the check below
- && matchesExpectedBinaryDictFormatVersionForThisType(
- mBinaryDictionary.getFormatVersion()))) {
- // Binary dictionary or its format version is not valid. Regenerate
- // the dictionary file. createNewDictionaryLocked will remove the
- // existing files if appropriate.
- createNewDictionaryLocked();
- }
}
- mNeedsToRecreate = false;
- } finally {
- mIsReloading.set(false);
}
+ clearNeedsToRecreate();
+ } finally {
+ isReloading.set(false);
}
- });
- }
+ }
+ });
}
/**
@@ -632,22 +692,62 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
- if (mBinaryDictionary == null) {
+ final BinaryDictionary binaryDictionary = getBinaryDictionary();
+ if (binaryDictionary == null) {
return;
}
- if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
- mBinaryDictionary.flushWithGC();
+ if (binaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
+ binaryDictionary.flushWithGC();
} else {
- mBinaryDictionary.flush();
+ binaryDictionary.flush();
}
}
});
}
+ static int parseEntryCount(final String entryCountStr) {
+ int entryCount;
+ try {
+ entryCount = Integer.parseInt(entryCountStr);
+ } catch (final NumberFormatException e) {
+ entryCount = DictionaryStats.NOT_AN_ENTRY_COUNT;
+ }
+ return entryCount;
+ }
+
+ public DictionaryStats getDictionaryStats() {
+ reloadDictionaryIfRequired();
+ final String dictName = mDictName;
+ final File dictFile = mDictFile;
+ final AsyncResultHolder<DictionaryStats> result = new AsyncResultHolder<>();
+ asyncExecuteTaskWithLock(mLock.readLock(), dictName /* executorName */, new Runnable() {
+ @Override
+ public void run() {
+ final BinaryDictionary binaryDictionary = getBinaryDictionary();
+ if (binaryDictionary == null) {
+ result.set(new DictionaryStats(mLocale, dictName, dictFile,
+ DictionaryStats.NOT_AN_ENTRY_COUNT,
+ DictionaryStats.NOT_AN_ENTRY_COUNT));
+ return;
+ }
+ final int unigramCount = parseEntryCount(
+ binaryDictionary.getPropertyForGettingStats(
+ BinaryDictionary.MAX_UNIGRAM_COUNT_QUERY));
+ // TODO: Get dedicated entry counts for bigram, trigram, and so on.
+ final int ngramCount = parseEntryCount(binaryDictionary.getPropertyForGettingStats(
+ BinaryDictionary.MAX_BIGRAM_COUNT_QUERY));
+ // TODO: Get more information from dictionary.
+ result.set(new DictionaryStats(mLocale, dictName, dictFile, unigramCount,
+ ngramCount));
+ }
+ });
+ return result.get(null /* defaultValue */, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
+ }
+
@UsedForTesting
public void waitAllTasksForTests() {
final CountDownLatch countDownLatch = new CountDownLatch(1);
- ExecutorUtils.getExecutor(mDictName).execute(new Runnable() {
+ asyncExecuteTaskWithWriteLock(new Runnable() {
@Override
public void run() {
countDownLatch.countDown();
@@ -669,28 +769,34 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
public void dumpAllWordsForDebug() {
reloadDictionaryIfRequired();
- asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() {
+ final String tag = TAG;
+ final String dictName = mDictName;
+ asyncExecuteTaskWithLock(mLock.readLock(), "dumpAllWordsForDebug", new Runnable() {
@Override
public void run() {
- Log.d(TAG, "Dump dictionary: " + mDictName);
+ Log.d(tag, "Dump dictionary: " + dictName + " for " + mLocale);
+ final BinaryDictionary binaryDictionary = getBinaryDictionary();
+ if (binaryDictionary == null) {
+ return;
+ }
try {
- final DictionaryHeader header = mBinaryDictionary.getHeader();
- Log.d(TAG, "Format version: " + mBinaryDictionary.getFormatVersion());
- Log.d(TAG, CombinedFormatUtils.formatAttributeMap(
+ final DictionaryHeader header = binaryDictionary.getHeader();
+ Log.d(tag, "Format version: " + binaryDictionary.getFormatVersion());
+ Log.d(tag, CombinedFormatUtils.formatAttributeMap(
header.mDictionaryOptions.mAttributes));
} catch (final UnsupportedFormatException e) {
- Log.d(TAG, "Cannot fetch header information.", e);
+ Log.d(tag, "Cannot fetch header information.", e);
}
int token = 0;
do {
final BinaryDictionary.GetNextWordPropertyResult result =
- mBinaryDictionary.getNextWordProperty(token);
+ binaryDictionary.getNextWordProperty(token);
final WordProperty wordProperty = result.mWordProperty;
if (wordProperty == null) {
- Log.d(TAG, " dictionary is empty.");
+ Log.d(tag, " dictionary is empty.");
break;
}
- Log.d(TAG, wordProperty.toString());
+ Log.d(tag, wordProperty.toString());
token = result.mNextToken;
} while (token != 0);
}
diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java
index fecb0ef94..37effeead 100644
--- a/java/src/com/android/inputmethod/latin/InputAttributes.java
+++ b/java/src/com/android/inputmethod/latin/InputAttributes.java
@@ -16,15 +16,16 @@
package com.android.inputmethod.latin;
-import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
-import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
+import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_FLOATING_GESTURE_PREVIEW;
+import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_MICROPHONE;
+import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_MICROPHONE_COMPAT;
import android.text.InputType;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.utils.InputTypeUtils;
-import com.android.inputmethod.latin.utils.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
@@ -42,6 +43,12 @@ public final class InputAttributes {
final public boolean mApplicationSpecifiedCompletionOn;
final public boolean mShouldInsertSpacesAutomatically;
final public boolean mShouldShowVoiceInputKey;
+ /**
+ * Whether the floating gesture preview should be disabled. If true, this should override the
+ * corresponding keyboard settings preference, always suppressing the floating preview text.
+ * {@link com.android.inputmethod.latin.settings.SettingsValues#mGestureFloatingPreviewTextEnabled}
+ */
+ final public boolean mDisableGestureFloatingPreviewText;
final public boolean mIsGeneralTextInput;
final private int mInputType;
final private EditorInfo mEditorInfo;
@@ -77,6 +84,7 @@ public final class InputAttributes {
mApplicationSpecifiedCompletionOn = false;
mShouldInsertSpacesAutomatically = false;
mShouldShowVoiceInputKey = false;
+ mDisableGestureFloatingPreviewText = false;
mIsGeneralTextInput = false;
return;
}
@@ -109,6 +117,9 @@ public final class InputAttributes {
|| hasNoMicrophoneKeyOption();
mShouldShowVoiceInputKey = !noMicrophone;
+ mDisableGestureFloatingPreviewText = InputAttributes.inPrivateImeOptions(
+ mPackageNameForPrivateImeOptions, NO_FLOATING_GESTURE_PREVIEW, editorInfo);
+
// If it's a browser edit field and auto correct is not ON explicitly, then
// disable auto correction, but keep suggestions on.
// If NO_SUGGESTIONS is set, don't do prediction.
diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java
deleted file mode 100644
index 790e0d830..000000000
--- a/java/src/com/android/inputmethod/latin/InputPointers.java
+++ /dev/null
@@ -1,183 +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.util.Log;
-import android.util.SparseIntArray;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.define.DebugFlags;
-import com.android.inputmethod.latin.utils.ResizableIntArray;
-
-// TODO: This class is not thread-safe.
-public final class InputPointers {
- private static final String TAG = InputPointers.class.getSimpleName();
- private static final boolean DEBUG_TIME = false;
-
- 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);
- }
-
- private void fillWithLastTimeUntil(final int index) {
- final int fromIndex = mTimes.getLength();
- // Fill the gap with the latest time.
- // See {@link #getTime(int)} and {@link #isValidTimeStamps()}.
- if (fromIndex <= 0) {
- return;
- }
- final int fillLength = index - fromIndex + 1;
- if (fillLength <= 0) {
- return;
- }
- final int lastTime = mTimes.get(fromIndex - 1);
- mTimes.fill(lastTime, fromIndex, fillLength);
- }
-
- public void addPointerAt(int index, int x, int y, int pointerId, int time) {
- mXCoordinates.addAt(index, x);
- mYCoordinates.addAt(index, y);
- mPointerIds.addAt(index, pointerId);
- if (DebugFlags.DEBUG_ENABLED || DEBUG_TIME) {
- fillWithLastTimeUntil(index);
- }
- mTimes.addAt(index, time);
- }
-
- @UsedForTesting
- 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 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);
- }
-
- /**
- * Shift to the left by elementCount, discarding elementCount pointers at the start.
- * @param elementCount how many elements to shift.
- */
- public void shift(final int elementCount) {
- mXCoordinates.shift(elementCount);
- mYCoordinates.shift(elementCount);
- mPointerIds.shift(elementCount);
- mTimes.shift(elementCount);
- }
-
- 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() {
- if (DebugFlags.DEBUG_ENABLED || DEBUG_TIME) {
- if (!isValidTimeStamps()) {
- throw new RuntimeException("Time stamps are invalid.");
- }
- }
- return mTimes.getPrimitiveArray();
- }
-
- @Override
- public String toString() {
- return "size=" + getPointerSize() + " id=" + mPointerIds + " time=" + mTimes
- + " x=" + mXCoordinates + " y=" + mYCoordinates;
- }
-
- private boolean isValidTimeStamps() {
- final int[] times = mTimes.getPrimitiveArray();
- final int[] pointerIds = mPointerIds.getPrimitiveArray();
- final SparseIntArray lastTimeOfPointers = new SparseIntArray();
- final int size = getPointerSize();
- for (int i = 0; i < size; ++i) {
- final int pointerId = pointerIds[i];
- final int time = times[i];
- final int lastTime = lastTimeOfPointers.get(pointerId, time);
- if (time < lastTime) {
- // dump
- for (int j = 0; j < size; ++j) {
- Log.d(TAG, "--- (" + j + ") " + times[j]);
- }
- return false;
- }
- lastTimeOfPointers.put(pointerId, time);
- }
- return true;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java
index 7fa935413..f3a8ca169 100644
--- a/java/src/com/android/inputmethod/latin/InputView.java
+++ b/java/src/com/android/inputmethod/latin/InputView.java
@@ -139,7 +139,10 @@ public final class InputView extends FrameLayout {
return y - mEventReceivingRect.top;
}
- // Callback when a {@link MotionEvent} is forwarded.
+ /**
+ * Callback when a {@link MotionEvent} is forwarded.
+ * @param me the motion event to be forwarded.
+ */
protected void onForwardingEvent(final MotionEvent me) {}
// Returns true if a {@link MotionEvent} is needed to be forwarded to
diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java
index 8cbf8379b..9fcdb2229 100644
--- a/java/src/com/android/inputmethod/latin/LastComposedWord.java
+++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java
@@ -19,6 +19,8 @@ package com.android.inputmethod.latin;
import android.text.TextUtils;
import com.android.inputmethod.event.Event;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.InputPointers;
import java.util.ArrayList;
@@ -48,7 +50,7 @@ public final class LastComposedWord {
public final String mTypedWord;
public final CharSequence mCommittedWord;
public final String mSeparatorString;
- public final PrevWordsInfo mPrevWordsInfo;
+ public final NgramContext mNgramContext;
public final int mCapitalizedMode;
public final InputPointers mInputPointers =
new InputPointers(Constants.DICTIONARY_MAX_WORD_LENGTH);
@@ -64,7 +66,7 @@ public final class LastComposedWord {
public LastComposedWord(final ArrayList<Event> events,
final InputPointers inputPointers, final String typedWord,
final CharSequence committedWord, final String separatorString,
- final PrevWordsInfo prevWordsInfo, final int capitalizedMode) {
+ final NgramContext ngramContext, final int capitalizedMode) {
if (inputPointers != null) {
mInputPointers.copy(inputPointers);
}
@@ -73,7 +75,7 @@ public final class LastComposedWord {
mCommittedWord = committedWord;
mSeparatorString = separatorString;
mActive = true;
- mPrevWordsInfo = prevWordsInfo;
+ mNgramContext = ngramContext;
mCapitalizedMode = capitalizedMode;
}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index d57db8e9a..3fa127005 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -16,10 +16,11 @@
package com.android.inputmethod.latin;
-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 static com.android.inputmethod.latin.common.Constants.ImeOption.FORCE_ASCII;
+import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_MICROPHONE;
+import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_MICROPHONE_COMPAT;
+import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -32,6 +33,7 @@ import android.content.res.Resources;
import android.inputmethodservice.InputMethodService;
import android.media.AudioManager;
import android.net.ConnectivityManager;
+import android.os.Build;
import android.os.Debug;
import android.os.IBinder;
import android.os.Message;
@@ -59,6 +61,8 @@ import com.android.inputmethod.accessibility.AccessibilityUtils;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper;
import com.android.inputmethod.compat.InputMethodServiceCompatUtils;
+import com.android.inputmethod.compat.ViewOutlineProviderCompatUtils;
+import com.android.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater;
import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
import com.android.inputmethod.event.Event;
import com.android.inputmethod.event.HardwareEventDecoder;
@@ -72,6 +76,8 @@ import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.keyboard.TextDecoratorUi;
import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.InputPointers;
import com.android.inputmethod.latin.define.DebugFlags;
import com.android.inputmethod.latin.define.ProductionFlags;
import com.android.inputmethod.latin.inputlogic.InputLogic;
@@ -84,27 +90,31 @@ import com.android.inputmethod.latin.settings.SettingsActivity;
import com.android.inputmethod.latin.settings.SettingsValues;
import com.android.inputmethod.latin.suggestions.SuggestionStripView;
import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
+import com.android.inputmethod.latin.touchinputconsumer.GestureConsumer;
import com.android.inputmethod.latin.utils.ApplicationUtils;
import com.android.inputmethod.latin.utils.CapsModeUtils;
import com.android.inputmethod.latin.utils.CoordinateUtils;
import com.android.inputmethod.latin.utils.CursorAnchorInfoUtils;
import com.android.inputmethod.latin.utils.DialogUtils;
-import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions;
import com.android.inputmethod.latin.utils.ImportantNoticeUtils;
import com.android.inputmethod.latin.utils.IntentUtils;
import com.android.inputmethod.latin.utils.JniUtils;
import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper;
import com.android.inputmethod.latin.utils.StatsUtils;
+import com.android.inputmethod.latin.utils.StatsUtilsManager;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
import com.android.inputmethod.latin.utils.ViewLayoutUtils;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
+import javax.annotation.Nonnull;
+
/**
* Input method implementation for Qwerty'ish keyboard.
*/
@@ -112,17 +122,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
SuggestionStripView.Listener, SuggestionStripViewAccessor,
DictionaryFacilitator.DictionaryInitializationListener,
ImportantNoticeDialog.ImportantNoticeDialogListener {
- private static final String TAG = LatinIME.class.getSimpleName();
+ static final String TAG = LatinIME.class.getSimpleName();
private static final boolean TRACE = false;
private static boolean DEBUG = false;
private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
-
- private static final int PENDING_IMS_CALLBACK_DURATION = 800;
-
- private static final int DELAY_WAIT_FOR_DICTIONARY_LOAD = 2000; // 2s
-
private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2;
+ private static final int PENDING_IMS_CALLBACK_DURATION_MILLIS = 800;
+ static final long DELAY_WAIT_FOR_DICTIONARY_LOAD_MILLIS = TimeUnit.SECONDS.toMillis(2);
+ static final long DELAY_DEALLOCATE_MEMORY_MILLIS = TimeUnit.SECONDS.toMillis(10);
/**
* The name of the scheme used by the Package Manager to warn of a new package installation,
@@ -130,10 +138,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
*/
private static final String SCHEME_PACKAGE = "package";
- private final Settings mSettings;
+ final Settings mSettings;
private final DictionaryFacilitator mDictionaryFacilitator =
- new DictionaryFacilitator(
- new DistracterFilterCheckingExactMatchesAndSuggestions(this /* context */));
+ new DictionaryFacilitator(this /* context */);
// TODO: Move from LatinIME.
private final PersonalizationDictionaryUpdater mPersonalizationDictionaryUpdater =
new PersonalizationDictionaryUpdater(this /* context */, mDictionaryFacilitator);
@@ -145,7 +152,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE);
}
});
- private final InputLogic mInputLogic = new InputLogic(this /* LatinIME */,
+ final InputLogic mInputLogic = new InputLogic(this /* LatinIME */,
this /* SuggestionStripViewAccessor */, mDictionaryFacilitator);
// We expect to have only one decoder in almost all cases, hence the default capacity of 1.
// If it turns out we need several, it will get grown seamlessly.
@@ -153,14 +160,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// TODO: Move these {@link View}s to {@link KeyboardSwitcher}.
private View mInputView;
+ private InsetsUpdater mInsetsUpdater;
private SuggestionStripView mSuggestionStripView;
private TextView mExtractEditText;
private RichInputMethodManager mRichImm;
@UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
- private final SubtypeSwitcher mSubtypeSwitcher;
+ final SubtypeSwitcher mSubtypeSwitcher;
private final SubtypeState mSubtypeState = new SubtypeState();
- private final SpecialKeyDetector mSpecialKeyDetector;
+ private final EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector =
+ new EmojiAltPhysicalKeyDetector();
+ private StatsUtilsManager mStatsUtilsManager;
// Working variable for {@link #startShowingInputView()} and
// {@link #onEvaluateInputViewShown()}.
private boolean mIsExecutingStartShowingInputView;
@@ -176,6 +186,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private final boolean mIsHardwareAcceleratedDrawingEnabled;
+ private GestureConsumer mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER;
+
public final UIHandler mHandler = new UIHandler(this);
public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> {
@@ -188,20 +200,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private static final int MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED = 6;
private static final int MSG_RESET_CACHES = 7;
private static final int MSG_WAIT_FOR_DICTIONARY_LOAD = 8;
+ private static final int MSG_DEALLOCATE_MEMORY = 9;
// Update this when adding new messages
- private static final int MSG_LAST = MSG_WAIT_FOR_DICTIONARY_LOAD;
+ private static final int MSG_LAST = MSG_DEALLOCATE_MEMORY;
private static final int ARG1_NOT_GESTURE_INPUT = 0;
private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2;
private static final int ARG2_UNUSED = 0;
- private static final int ARG1_FALSE = 0;
private static final int ARG1_TRUE = 1;
private int mDelayInMillisecondsToUpdateSuggestions;
private int mDelayInMillisecondsToUpdateShiftState;
- public UIHandler(final LatinIME ownerInstance) {
+ public UIHandler(@Nonnull final LatinIME ownerInstance) {
super(ownerInstance);
}
@@ -246,19 +258,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
case MSG_RESUME_SUGGESTIONS:
latinIme.mInputLogic.restartSuggestionsOnWordTouchedByCursor(
latinIme.mSettings.getCurrent(),
- msg.arg1 == ARG1_TRUE /* shouldIncludeResumedWordInSuggestions */,
latinIme.mKeyboardSwitcher.getCurrentKeyboardScriptId());
break;
case MSG_REOPEN_DICTIONARIES:
// We need to re-evaluate the currently composing word in case the script has
// changed.
postWaitForDictionaryLoad();
- latinIme.resetSuggest();
+ latinIme.resetDictionaryFacilitatorIfNecessary();
break;
case MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED:
+ final SuggestedWords suggestedWords = (SuggestedWords) msg.obj;
latinIme.mInputLogic.onUpdateTailBatchInputCompleted(
latinIme.mSettings.getCurrent(),
- (SuggestedWords) msg.obj, latinIme.mKeyboardSwitcher);
+ suggestedWords, latinIme.mKeyboardSwitcher);
+ latinIme.onTailBatchInputResultShown(suggestedWords);
break;
case MSG_RESET_CACHES:
final SettingsValues settingsValues = latinIme.mSettings.getCurrent();
@@ -275,6 +288,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
case MSG_WAIT_FOR_DICTIONARY_LOAD:
Log.i(TAG, "Timeout waiting for dictionary load");
break;
+ case MSG_DEALLOCATE_MEMORY:
+ latinIme.deallocateMemory();
+ break;
}
}
@@ -287,8 +303,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES));
}
- public void postResumeSuggestions(final boolean shouldIncludeResumedWordInSuggestions,
- final boolean shouldDelay) {
+ public void postResumeSuggestions(final boolean shouldDelay) {
final LatinIME latinIme = getOwnerInstance();
if (latinIme == null) {
return;
@@ -298,13 +313,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
removeMessages(MSG_RESUME_SUGGESTIONS);
if (shouldDelay) {
- sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS,
- shouldIncludeResumedWordInSuggestions ? ARG1_TRUE : ARG1_FALSE,
- 0 /* ignored */), mDelayInMillisecondsToUpdateSuggestions);
+ sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS),
+ mDelayInMillisecondsToUpdateSuggestions);
} else {
- sendMessage(obtainMessage(MSG_RESUME_SUGGESTIONS,
- shouldIncludeResumedWordInSuggestions ? ARG1_TRUE : ARG1_FALSE,
- 0 /* ignored */));
+ sendMessage(obtainMessage(MSG_RESUME_SUGGESTIONS));
}
}
@@ -316,7 +328,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
public void postWaitForDictionaryLoad() {
sendMessageDelayed(obtainMessage(MSG_WAIT_FOR_DICTIONARY_LOAD),
- DELAY_WAIT_FOR_DICTIONARY_LOAD);
+ DELAY_WAIT_FOR_DICTIONARY_LOAD_MILLIS);
}
public void cancelWaitForDictionaryLoad() {
@@ -345,6 +357,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mDelayInMillisecondsToUpdateShiftState);
}
+ public void postDeallocateMemory() {
+ sendMessageDelayed(obtainMessage(MSG_DEALLOCATE_MEMORY),
+ DELAY_DEALLOCATE_MEMORY_MILLIS);
+ }
+
+ public void cancelDeallocateMemory() {
+ removeMessages(MSG_DEALLOCATE_MEMORY);
+ }
+
+ public boolean hasPendingDeallocateMemory() {
+ return hasMessages(MSG_DEALLOCATE_MEMORY);
+ }
+
@UsedForTesting
public void removeAllMessages() {
for (int i = 0; i <= MSG_LAST; ++i) {
@@ -442,7 +467,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mPendingSuccessiveImsCallback = false;
resetPendingImsCallback();
sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
- PENDING_IMS_CALLBACK_DURATION);
+ PENDING_IMS_CALLBACK_DURATION_MILLIS);
}
final LatinIME latinIme = getOwnerInstance();
if (latinIme != null) {
@@ -450,6 +475,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
latinIme.onStartInputViewInternal(editorInfo, restarting);
mAppliedEditorInfo = editorInfo;
}
+ cancelDeallocateMemory();
}
}
@@ -463,6 +489,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
latinIme.onFinishInputViewInternal(finishingInput);
mAppliedEditorInfo = null;
}
+ if (!hasPendingDeallocateMemory()) {
+ postDeallocateMemory();
+ }
}
}
@@ -518,7 +547,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mSettings = Settings.getInstance();
mSubtypeSwitcher = SubtypeSwitcher.getInstance();
mKeyboardSwitcher = KeyboardSwitcher.getInstance();
- mSpecialKeyDetector = new SpecialKeyDetector(this);
+ mStatsUtilsManager = StatsUtilsManager.getInstance();
mIsHardwareAcceleratedDrawingEnabled =
InputMethodServiceCompatUtils.enableHardwareAcceleration(this);
Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled);
@@ -534,16 +563,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
KeyboardSwitcher.init(this);
AudioAndHapticFeedbackManager.init(this);
AccessibilityUtils.init(this);
- StatsUtils.init(this);
-
+ mStatsUtilsManager.onCreate(this /* context */);
super.onCreate();
mHandler.onCreate();
DEBUG = DebugFlags.DEBUG_ENABLED;
- // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}.
+ // TODO: Resolve mutual dependencies of {@link #loadSettings()} and
+ // {@link #resetDictionaryFacilitatorIfNecessary()}.
loadSettings();
- resetSuggest();
+ mSubtypeSwitcher.onSubtypeChanged(mRichImm.getCurrentRawSubtype());
+ resetDictionaryFacilitatorIfNecessary();
// Register to receive ringer mode change and network state change.
// Also receive installation and removal of a dictionary pack.
@@ -567,37 +597,38 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
registerReceiver(mDictionaryDumpBroadcastReceiver, dictDumpFilter);
DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this);
-
- StatsUtils.onCreate(mSettings.getCurrent());
+ StatsUtils.onCreate(mSettings.getCurrent(), mRichImm);
}
// Has to be package-visible for unit tests
@UsedForTesting
void loadSettings() {
- final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
+ final Locale[] locales = mSubtypeSwitcher.getCurrentSubtypeLocales();
final EditorInfo editorInfo = getCurrentInputEditorInfo();
final InputAttributes inputAttributes = new InputAttributes(
editorInfo, isFullscreenMode(), getPackageName());
- mSettings.loadSettings(this, locale, inputAttributes);
+ // TODO: pass the array instead
+ mSettings.loadSettings(this, locales[0], inputAttributes);
final SettingsValues currentSettingsValues = mSettings.getCurrent();
AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(currentSettingsValues);
// This method is called on startup and language switch, before the new layout has
// been displayed. Opening dictionaries never affects responsivity as dictionaries are
// asynchronously loaded.
if (!mHandler.hasPendingReopenDictionaries()) {
- resetSuggestForLocale(locale);
+ resetDictionaryFacilitatorForLocale(locales);
}
mDictionaryFacilitator.updateEnabledSubtypes(mRichImm.getMyEnabledInputMethodSubtypeList(
true /* allowsImplicitlySelectedSubtypes */));
refreshPersonalizationDictionarySession(currentSettingsValues);
- StatsUtils.onLoadSettings(currentSettingsValues);
+ mStatsUtilsManager.onLoadSettings(currentSettingsValues);
}
private void refreshPersonalizationDictionarySession(
final SettingsValues currentSettingsValues) {
- mPersonalizationDictionaryUpdater.onLoadSettings(
- currentSettingsValues.mUsePersonalizedDicts,
+ mDictionaryFacilitator.setIsMonolingualUser(
mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes());
+ mPersonalizationDictionaryUpdater.onLoadSettings(
+ currentSettingsValues.mUsePersonalizedDicts);
mContextualDictionaryUpdater.onLoadSettings(currentSettingsValues.mUsePersonalizedDicts);
final boolean shouldKeepUserHistoryDictionaries;
if (currentSettingsValues.mUsePersonalizedDicts) {
@@ -621,38 +652,38 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
if (mHandler.hasPendingWaitForDictionaryLoad()) {
mHandler.cancelWaitForDictionaryLoad();
- mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */,
- false /* shouldDelay */);
+ mHandler.postResumeSuggestions(false /* shouldDelay */);
}
}
- private void resetSuggest() {
- final Locale switcherSubtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
- final String switcherLocaleStr = switcherSubtypeLocale.toString();
- final Locale subtypeLocale;
- if (TextUtils.isEmpty(switcherLocaleStr)) {
+ void resetDictionaryFacilitatorIfNecessary() {
+ final Locale[] subtypeSwitcherLocales = mSubtypeSwitcher.getCurrentSubtypeLocales();
+ if (mDictionaryFacilitator.isForLocales(subtypeSwitcherLocales)) {
+ return;
+ }
+ final Locale[] subtypeLocales;
+ if (0 == subtypeSwitcherLocales.length) {
// This happens in very rare corner cases - for example, immediately after a switch
// to LatinIME has been requested, about a frame later another switch happens. In this
// case, we are about to go down but we still don't know it, however the system tells
- // us there is no current subtype so the locale is the empty string. Take the best
- // possible guess instead -- it's bound to have no consequences, and we have no way
- // of knowing anyway.
+ // us there is no current subtype.
Log.e(TAG, "System is reporting no current subtype.");
- subtypeLocale = getResources().getConfiguration().locale;
+ subtypeLocales = new Locale[] { getResources().getConfiguration().locale };
} else {
- subtypeLocale = switcherSubtypeLocale;
+ subtypeLocales = subtypeSwitcherLocales;
}
- resetSuggestForLocale(subtypeLocale);
+ resetDictionaryFacilitatorForLocale(subtypeLocales);
}
/**
- * Reset suggest by loading dictionaries for the locale and the current settings values.
+ * Reset the facilitator by loading dictionaries for the locales and the current settings values
*
- * @param locale the locale
+ * @param locales the locales
*/
- private void resetSuggestForLocale(final Locale locale) {
+ // TODO: make sure the current settings always have the right locales, and read from them
+ private void resetDictionaryFacilitatorForLocale(final Locale[] locales) {
final SettingsValues settingsValues = mSettings.getCurrent();
- mDictionaryFacilitator.resetDictionaries(this /* context */, locale,
+ mDictionaryFacilitator.resetDictionaries(this /* context */, locales,
settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts,
false /* forceReloadMainDictionary */, this);
if (settingsValues.mAutoCorrectionEnabledPerUserSettings) {
@@ -667,7 +698,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
/* package private */ void resetSuggestMainDict() {
final SettingsValues settingsValues = mSettings.getCurrent();
mDictionaryFacilitator.resetDictionaries(this /* context */,
- mDictionaryFacilitator.getLocale(), settingsValues.mUseContactsDict,
+ mDictionaryFacilitator.getLocales(), settingsValues.mUseContactsDict,
settingsValues.mUsePersonalizedDicts, true /* forceReloadMainDictionary */, this);
}
@@ -680,7 +711,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
unregisterReceiver(mConnectivityAndRingerModeChangeReceiver);
unregisterReceiver(mDictionaryPackInstallReceiver);
unregisterReceiver(mDictionaryDumpBroadcastReceiver);
- StatsUtils.onDestroy();
+ mStatsUtilsManager.onDestroy();
super.onDestroy();
}
@@ -715,15 +746,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
cleanupInternalStateForFinishInput();
}
}
- // TODO: Remove this test.
- if (!conf.locale.equals(mPersonalizationDictionaryUpdater.getLocale())) {
- refreshPersonalizationDictionarySession(settingsValues);
- }
super.onConfigurationChanged(conf);
}
@Override
public View onCreateInputView() {
+ StatsUtils.onCreateInputView();
return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled);
}
@@ -731,6 +759,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
public void setInputView(final View view) {
super.setInputView(view);
mInputView = view;
+ mInsetsUpdater = ViewOutlineProviderCompatUtils.setInsetsOutlineProvider(view);
+ updateSoftInputWindowLayoutParameters();
mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
if (hasSuggestionStripView()) {
mSuggestionStripView.setListener(this, view);
@@ -752,35 +782,36 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (prevExtractEditText == nextExtractEditText) {
return;
}
- if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && prevExtractEditText != null) {
+ if (prevExtractEditText != null) {
prevExtractEditText.getViewTreeObserver().removeOnPreDrawListener(
mExtractTextViewPreDrawListener);
}
mExtractEditText = nextExtractEditText;
- if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && mExtractEditText != null) {
+ if (mExtractEditText != null) {
mExtractEditText.getViewTreeObserver().addOnPreDrawListener(
mExtractTextViewPreDrawListener);
}
}
+ void updateCursorAnchorInfo() {
+ // CursorAnchorInfo is used on L and later.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ if (isFullscreenMode() && mExtractEditText != null) {
+ mInputLogic.onUpdateCursorAnchorInfo(
+ CursorAnchorInfoUtils.extractFromTextView(mExtractEditText));
+ }
+ }
+ }
+
private final ViewTreeObserver.OnPreDrawListener mExtractTextViewPreDrawListener =
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
- onExtractTextViewPreDraw();
+ updateCursorAnchorInfo();
return true;
}
};
- private void onExtractTextViewPreDraw() {
- if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || !isFullscreenMode()
- || mExtractEditText == null) {
- return;
- }
- final CursorAnchorInfo info = CursorAnchorInfoUtils.getCursorAnchorInfo(mExtractEditText);
- mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info));
- }
-
@Override
public void setCandidatesView(final View view) {
// To ensure that CandidatesView will never be set.
@@ -795,11 +826,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
mHandler.onStartInputView(editorInfo, restarting);
+ mStatsUtilsManager.onStartInputView();
}
@Override
public void onFinishInputView(final boolean finishingInput) {
+ StatsUtils.onFinishInputView();
mHandler.onFinishInputView(finishingInput);
+ mStatsUtilsManager.onFinishInputView();
+ mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER;
}
@Override
@@ -817,14 +852,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
loadKeyboard();
}
- private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
+ void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
super.onStartInput(editorInfo, restarting);
}
@SuppressWarnings("deprecation")
- private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
+ void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
super.onStartInputView(editorInfo, restarting);
+ // Switch to the null consumer to handle cases leading to early exit below, for which we
+ // also wouldn't be consuming gesture data.
+ mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER;
mRichImm.clearSubtypeCaches();
+ mSubtypeSwitcher.refreshSubtypeInfo();
final KeyboardSwitcher switcher = mKeyboardSwitcher;
switcher.updateKeyboardTheme();
final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
@@ -867,6 +906,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return;
}
+ // Update to a gesture consumer with the current editor and IME state.
+ mGestureConsumer = GestureConsumer.newInstance(editorInfo,
+ mInputLogic.getPrivateCommandPerformer(),
+ Arrays.asList(mSubtypeSwitcher.getCurrentSubtypeLocales()),
+ switcher.getKeyboard());
+
// Forward this event to the accessibility utilities, if enabled.
final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
if (accessUtils.isTouchExplorationEnabled()) {
@@ -875,6 +920,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo);
final boolean isDifferentTextField = !restarting || inputTypeChanged;
+
+ StatsUtils.onStartInputView(editorInfo.inputType,
+ Settings.getInstance().getCurrent().mDisplayOrientation,
+ !isDifferentTextField);
+
if (isDifferentTextField) {
mSubtypeSwitcher.updateParametersOnStartInputView();
}
@@ -900,12 +950,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype(),
currentSettingsValues);
- // Note: the following does a round-trip IPC on the main thread: be careful
- final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
- if (null != currentLocale && !currentLocale.equals(suggest.getLocale())) {
- // TODO: Do this automatically.
- resetSuggest();
- }
+ resetDictionaryFacilitatorIfNecessary();
// TODO[IL]: Can the following be moved to InputLogic#startInput?
if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
@@ -919,11 +964,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// mLastSelection{Start,End} are reset later in this method, no need to do it here
needToCallLoadKeyboardLater = true;
} else {
- // When rotating, initialSelStart and initialSelEnd sometimes are lying. Make a best
- // effort to work around this bug.
+ // When rotating, and when input is starting again in a field from where the focus
+ // didn't move (the keyboard having been closed with the back key),
+ // initialSelStart and initialSelEnd sometimes are lying. Make a best effort to
+ // work around this bug.
mInputLogic.mConnection.tryFixLyingCursorPosition();
- mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */,
- true /* shouldDelay */);
+ mHandler.postResumeSuggestions(true /* shouldDelay */);
needToCallLoadKeyboardLater = false;
}
} else {
@@ -970,7 +1016,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mHandler.cancelUpdateSuggestionStrip();
mainKeyboardView.setMainDictionaryAvailability(
- mDictionaryFacilitator.hasInitializedMainDictionary());
+ mDictionaryFacilitator.hasAtLeastOneInitializedMainDictionary());
mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn,
currentSettingsValues.mKeyPreviewPopupDismissDelay);
mainKeyboardView.setSlidingKeyInputPreviewEnabled(
@@ -994,7 +1040,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- private void onFinishInputInternal() {
+ void onFinishInputInternal() {
super.onFinishInput();
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
@@ -1003,19 +1049,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- private void onFinishInputViewInternal(final boolean finishingInput) {
+ void onFinishInputViewInternal(final boolean finishingInput) {
super.onFinishInputView(finishingInput);
cleanupInternalStateForFinishInput();
}
private void cleanupInternalStateForFinishInput() {
- mKeyboardSwitcher.deallocateMemory();
// Remove pending messages related to update suggestions
mHandler.cancelUpdateSuggestionStrip();
// Should do the following in onFinishInputInternal but until JB MR2 it's not called :(
mInputLogic.finishInput();
}
+ protected void deallocateMemory() {
+ mKeyboardSwitcher.deallocateMemory();
+ }
+
@Override
public void onUpdateSelection(final int oldSelStart, final int oldSelEnd,
final int newSelStart, final int newSelEnd,
@@ -1040,13 +1089,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- // We cannot mark this method as @Override until new SDK becomes publicly available.
- // @Override
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
public void onUpdateCursorAnchorInfo(final CursorAnchorInfo info) {
- if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || isFullscreenMode()) {
+ if (isFullscreenMode()) {
return;
}
- mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info));
+ mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.wrap(info));
}
/**
@@ -1131,6 +1180,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onComputeInsets(final InputMethodService.Insets outInsets) {
super.onComputeInsets(outInsets);
+ // This method may be called before {@link #setInputView(View)}.
+ if (mInputView == null) {
+ return;
+ }
final SettingsValues settingsValues = mSettings.getCurrent();
final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
if (visibleKeyboardView == null || !hasSuggestionStripView()) {
@@ -1143,6 +1196,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// no visual element will be shown on the screen.
outInsets.touchableInsets = inputHeight;
outInsets.visibleTopInsets = inputHeight;
+ mInsetsUpdater.setInsets(outInsets);
return;
}
final int suggestionsHeight = (!mKeyboardSwitcher.isShowingEmojiPalettes()
@@ -1163,14 +1217,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
outInsets.contentTopInsets = visibleTopY;
outInsets.visibleTopInsets = visibleTopY;
+ mInsetsUpdater.setInsets(outInsets);
}
- public void startShowingInputView() {
+ public void startShowingInputView(final boolean needsToLoadKeyboard) {
mIsExecutingStartShowingInputView = true;
// This {@link #showWindow(boolean)} will eventually call back
// {@link #onEvaluateInputViewShown()}.
showWindow(true /* showInput */);
mIsExecutingStartShowingInputView = false;
+ if (needsToLoadKeyboard) {
+ loadKeyboard();
+ }
}
public void stopShowingInputView() {
@@ -1178,6 +1236,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
@Override
+ public boolean onShowInputRequested(final int flags, final boolean configChange) {
+ if (Settings.getInstance().getCurrent().mHasHardwareKeyboard) {
+ return true;
+ }
+ return super.onShowInputRequested(flags, configChange);
+ }
+
+ @Override
public boolean onEvaluateInputViewShown() {
if (mIsExecutingStartShowingInputView) {
return true;
@@ -1207,8 +1273,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void updateFullscreenMode() {
+ super.updateFullscreenMode();
+ mInputLogic.onUpdateFullscreenMode(isFullscreenMode());
+ updateSoftInputWindowLayoutParameters();
+ }
+
+ private void updateSoftInputWindowLayoutParameters() {
// Override layout parameters to expand {@link SoftInputWindow} to the entire screen.
- // See {@link InputMethodService#setinputView(View) and
+ // See {@link InputMethodService#setinputView(View)} and
// {@link SoftInputWindow#updateWidthHeight(WindowManager.LayoutParams)}.
final Window window = getWindow().getWindow();
ViewLayoutUtils.updateLayoutHeightOf(window, LayoutParams.MATCH_PARENT);
@@ -1227,22 +1299,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM);
ViewLayoutUtils.updateLayoutHeightOf(mInputView, layoutHeight);
}
- super.updateFullscreenMode();
- mInputLogic.onUpdateFullscreenMode(isFullscreenMode());
}
- private int getCurrentAutoCapsState() {
+ int getCurrentAutoCapsState() {
return mInputLogic.getCurrentAutoCapsState(mSettings.getCurrent());
}
- private int getCurrentRecapitalizeState() {
+ int getCurrentRecapitalizeState() {
return mInputLogic.getCurrentRecapitalizeState();
}
- public Locale getCurrentSubtypeLocale() {
- return mSubtypeSwitcher.getCurrentSubtypeLocale();
- }
-
/**
* @param codePoints code points to get coordinates for.
* @return x,y coordinates for this keyboard, as a flattened array.
@@ -1264,13 +1330,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Probably never supposed to happen, but just in case.
return;
}
- final String wordToEdit;
- if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) {
- wordToEdit = word.toLowerCase(getCurrentSubtypeLocale());
- } else {
- wordToEdit = word;
- }
- mDictionaryFacilitator.addWordToUserDictionary(this /* context */, wordToEdit);
+ mDictionaryFacilitator.addWordToUserDictionary(this /* context */, word);
mInputLogic.onAddWordToUserDictionary();
}
@@ -1328,48 +1388,56 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mSubtypeState.switchSubtype(token, mRichImm);
}
+ // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
+ // alphabetic shift and shift while in symbol layout and get rid of this method.
+ private int getCodePointForKeyboard(final int codePoint) {
+ if (Constants.CODE_SHIFT == codePoint) {
+ final Keyboard currentKeyboard = mKeyboardSwitcher.getKeyboard();
+ if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
+ return codePoint;
+ }
+ return Constants.CODE_SYMBOL_SHIFT;
+ }
+ return codePoint;
+ }
+
// Implementation of {@link KeyboardActionListener}.
@Override
public void onCodeInput(final int codePoint, final int x, final int y,
final boolean isKeyRepeat) {
+ // TODO: this processing does not belong inside LatinIME, the caller should be doing this.
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
// x and y include some padding, but everything down the line (especially native
// code) needs the coordinates in the keyboard frame.
// TODO: We should reconsider which coordinate system should be used to represent
// keyboard event. Also we should pull this up -- LatinIME has no business doing
- // this transformation, it should be done already before calling onCodeInput.
+ // this transformation, it should be done already before calling onEvent.
final int keyX = mainKeyboardView.getKeyX(x);
final int keyY = mainKeyboardView.getKeyY(y);
- final int codeToSend;
- if (Constants.CODE_SHIFT == codePoint) {
- // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
- // alphabetic shift and shift while in symbol layout.
- final Keyboard currentKeyboard = mKeyboardSwitcher.getKeyboard();
- if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
- codeToSend = codePoint;
- } else {
- codeToSend = Constants.CODE_SYMBOL_SHIFT;
- }
- } else {
- codeToSend = codePoint;
- }
- if (Constants.CODE_SHORTCUT == codePoint) {
+ final Event event = createSoftwareKeypressEvent(getCodePointForKeyboard(codePoint),
+ keyX, keyY, isKeyRepeat);
+ onEvent(event);
+ }
+
+ // This method is public for testability of LatinIME, but also in the future it should
+ // completely replace #onCodeInput.
+ public void onEvent(@Nonnull final Event event) {
+ if (Constants.CODE_SHORTCUT == event.mKeyCode) {
mSubtypeSwitcher.switchToShortcutIME(this);
- // Still call the *#onCodeInput methods for readability.
}
- final Event event = createSoftwareKeypressEvent(codeToSend, keyX, keyY, isKeyRepeat);
final InputTransaction completeInputTransaction =
mInputLogic.onCodeInput(mSettings.getCurrent(), event,
mKeyboardSwitcher.getKeyboardShiftMode(),
mKeyboardSwitcher.getCurrentKeyboardScriptId(), mHandler);
updateStateAfterInputTransaction(completeInputTransaction);
- mKeyboardSwitcher.onCodeInput(codePoint, getCurrentAutoCapsState(),
- getCurrentRecapitalizeState());
+ mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState());
}
// A helper method to split the code point and the key code. Ultimately, they should not be
// squashed into the same variable, and this method should be removed.
- private static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int keyX,
+ // public for testing, as we don't want to copy the same logic into test code
+ @Nonnull
+ public static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int keyX,
final int keyY, final boolean isKeyRepeat) {
final int keyCode;
final int codePoint;
@@ -1387,18 +1455,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onTextInput(final String rawText) {
// TODO: have the keyboard pass the correct key code when we need it.
- final Event event = Event.createSoftwareTextEvent(rawText, Event.NOT_A_KEY_CODE);
+ final Event event = Event.createSoftwareTextEvent(rawText, Constants.CODE_OUTPUT_TEXT);
final InputTransaction completeInputTransaction =
mInputLogic.onTextInput(mSettings.getCurrent(), event,
mKeyboardSwitcher.getKeyboardShiftMode(), mHandler);
updateStateAfterInputTransaction(completeInputTransaction);
- mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT, getCurrentAutoCapsState(),
- getCurrentRecapitalizeState());
+ mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState());
}
@Override
public void onStartBatchInput() {
mInputLogic.onStartBatchInput(mSettings.getCurrent(), mKeyboardSwitcher, mHandler);
+ mGestureConsumer.onGestureStarted(
+ Arrays.asList(mSubtypeSwitcher.getCurrentSubtypeLocales()),
+ mKeyboardSwitcher.getKeyboard());
}
@Override
@@ -1409,22 +1479,34 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onEndBatchInput(final InputPointers batchPointers) {
mInputLogic.onEndBatchInput(batchPointers);
+ mGestureConsumer.onGestureCompleted(batchPointers);
}
@Override
public void onCancelBatchInput() {
mInputLogic.onCancelBatchInput(mHandler);
+ mGestureConsumer.onGestureCanceled();
+ }
+
+ /**
+ * To be called after the InputLogic has gotten a chance to act on the on-device decoding
+ * for the full gesture, possibly updating the TextView to reflect the first decoding.
+ * <p>
+ * This method must be run on the UI Thread.
+ * @param suggestedWords On-device decoding for the full gesture.
+ */
+ public void onTailBatchInputResultShown(final SuggestedWords suggestedWords) {
+ mGestureConsumer.onImeSuggestionsProcessed(suggestedWords,
+ mInputLogic.getComposingStart(), mInputLogic.getComposingLength());
}
// This method must run on the UI Thread.
- private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
+ void showGesturePreviewAndSuggestionStrip(@Nonnull final SuggestedWords suggestedWords,
final boolean dismissGestureFloatingPreviewText) {
showSuggestionStrip(suggestedWords);
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
- mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
- if (dismissGestureFloatingPreviewText) {
- mainKeyboardView.dismissGestureFloatingPreviewText();
- }
+ mainKeyboardView.showGestureFloatingPreviewText(suggestedWords,
+ dismissGestureFloatingPreviewText /* dismissDelayed */);
}
// Called from PointerTracker through the KeyboardActionListener interface
@@ -1461,7 +1543,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private void setSuggestedWords(final SuggestedWords suggestedWords) {
final SettingsValues currentSettingsValues = mSettings.getCurrent();
- mInputLogic.setSuggestedWords(suggestedWords, currentSettingsValues, mHandler);
+ mInputLogic.setSuggestedWords(suggestedWords);
// TODO: Modify this when we support suggestions with hard keyboard
if (!hasSuggestionStripView()) {
return;
@@ -1489,7 +1571,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final boolean isEmptyApplicationSpecifiedCompletions =
currentSettingsValues.isApplicationSpecifiedCompletionsOn()
&& suggestedWords.isEmpty();
- final boolean noSuggestionsFromDictionaries = (SuggestedWords.EMPTY == suggestedWords)
+ final boolean noSuggestionsFromDictionaries = suggestedWords.isEmpty()
|| suggestedWords.isPunctuationSuggestions()
|| isEmptyApplicationSpecifiedCompletions;
final boolean isBeginningOfSentencePrediction = (suggestedWords.mInputStyle
@@ -1507,7 +1589,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// We should clear the contextual strip if there is no suggestion from dictionaries.
|| noSuggestionsFromDictionaries) {
mSuggestionStripView.setSuggestions(suggestedWords,
- SubtypeLocaleUtils.isRtlLanguage(mSubtypeSwitcher.getCurrentSubtype()));
+ mSubtypeSwitcher.getCurrentSubtype().isRtlSubtype());
}
}
@@ -1516,7 +1598,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final OnGetSuggestedWordsCallback callback) {
final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
if (keyboard == null) {
- callback.onGetSuggestedWords(SuggestedWords.EMPTY);
+ callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance());
return;
}
mInputLogic.getSuggestedWords(mSettings.getCurrent(), keyboard.getProximityInfo(),
@@ -1524,10 +1606,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
@Override
- public void showSuggestionStrip(final SuggestedWords sourceSuggestedWords) {
- final SuggestedWords suggestedWords =
- sourceSuggestedWords.isEmpty() ? SuggestedWords.EMPTY : sourceSuggestedWords;
- if (SuggestedWords.EMPTY == suggestedWords) {
+ public void showSuggestionStrip(final SuggestedWords suggestedWords) {
+ if (suggestedWords.isEmpty()) {
setNeutralSuggestionStrip();
} else {
setSuggestedWords(suggestedWords);
@@ -1535,7 +1615,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Cache the auto-correction in accessibility code so we can speak it if the user
// touches a key that will insert it.
AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords,
- sourceSuggestedWords.mTypedWord);
+ suggestedWords.mTypedWord);
}
// Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
@@ -1551,11 +1631,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
@Override
- public void showAddToDictionaryHint(final String word) {
+ public void suggestAddingToDictionary(final String word, final boolean isFromSuggestionStrip) {
if (!hasSuggestionStripView()) {
return;
}
- mSuggestionStripView.showAddToDictionaryHint(word);
+ final String wordToShow;
+ if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) {
+ wordToShow = word.toLowerCase(mDictionaryFacilitator.getMostProbableLocale());
+ } else {
+ wordToShow = word;
+ }
+ mSuggestionStripView.showAddToDictionaryHint(wordToShow,
+ isFromSuggestionStrip /* shouldShowWordToSave */);
}
// This will show either an empty suggestion strip (if prediction is enabled) or
@@ -1564,7 +1651,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
public void setNeutralSuggestionStrip() {
final SettingsValues currentSettings = mSettings.getCurrent();
final SuggestedWords neutralSuggestions = currentSettings.mBigramPredictionEnabled
- ? SuggestedWords.EMPTY : currentSettings.mSpacingAndPunctuations.mSuggestPuncList;
+ ? SuggestedWords.getEmptyInstance()
+ : currentSettings.mSpacingAndPunctuations.mSuggestPuncList;
setSuggestedWords(neutralSuggestions);
}
@@ -1676,7 +1764,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Hooks for hardware keyboard
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
- mSpecialKeyDetector.onKeyDown(keyEvent);
+ // TODO: This should be processed in {@link InputLogic}.
+ mEmojiAltPhysicalKeyDetector.onKeyDown(keyEvent);
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
return super.onKeyDown(keyCode, keyEvent);
}
@@ -1697,7 +1786,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public boolean onKeyUp(final int keyCode, final KeyEvent keyEvent) {
- mSpecialKeyDetector.onKeyUp(keyEvent);
+ // TODO: This should be processed in {@link InputLogic}.
+ mEmojiAltPhysicalKeyDetector.onKeyUp(keyEvent);
if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
return super.onKeyUp(keyCode, keyEvent);
}
@@ -1727,7 +1817,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
};
- private void launchSettings() {
+ void launchSettings() {
mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR);
requestHideSelf(0);
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
@@ -1751,6 +1841,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
languageSelectionTitle,
getString(ApplicationUtils.getActivityTitleResId(this, SettingsActivity.class))
};
+ final String imeId = mRichImm.getInputMethodIdOfThisIme();
final OnClickListener listener = new OnClickListener() {
@Override
public void onClick(DialogInterface di, int position) {
@@ -1758,7 +1849,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
switch (position) {
case 0:
final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
- mRichImm.getInputMethodIdOfThisIme(),
+ imeId,
Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -1816,7 +1907,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@UsedForTesting
/* package for test */ void replaceDictionariesForTest(final Locale locale) {
final SettingsValues settingsValues = mSettings.getCurrent();
- mDictionaryFacilitator.resetDictionaries(this, locale,
+ mDictionaryFacilitator.resetDictionaries(this, new Locale[] { locale },
settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts,
false /* forceReloadMainDictionary */, this /* listener */);
}
@@ -1835,8 +1926,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
public void dumpDictionaryForDebug(final String dictName) {
- if (mDictionaryFacilitator.getLocale() == null) {
- resetSuggest();
+ if (!mDictionaryFacilitator.isActive()) {
+ resetDictionaryFacilitatorIfNecessary();
}
mDictionaryFacilitator.dumpDictionaryForDebug(dictName);
}
diff --git a/java/src/com/android/inputmethod/latin/PrevWordsInfo.java b/java/src/com/android/inputmethod/latin/NgramContext.java
index db877ab7a..82a13274d 100644
--- a/java/src/com/android/inputmethod/latin/PrevWordsInfo.java
+++ b/java/src/com/android/inputmethod/latin/NgramContext.java
@@ -18,26 +18,34 @@ package com.android.inputmethod.latin;
import android.text.TextUtils;
-import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
import java.util.Arrays;
+import javax.annotation.Nonnull;
+
/**
* Class to represent information of previous words. This class is used to add n-gram entries
* into binary dictionaries, to get predictions, and to get suggestions.
*/
-public class PrevWordsInfo {
- public static final PrevWordsInfo EMPTY_PREV_WORDS_INFO =
- new PrevWordsInfo(WordInfo.EMPTY_WORD_INFO);
- public static final PrevWordsInfo BEGINNING_OF_SENTENCE =
- new PrevWordsInfo(WordInfo.BEGINNING_OF_SENTENCE);
+public class NgramContext {
+ @Nonnull
+ public static final NgramContext EMPTY_PREV_WORDS_INFO =
+ new NgramContext(WordInfo.EMPTY_WORD_INFO);
+ @Nonnull
+ public static final NgramContext BEGINNING_OF_SENTENCE =
+ new NgramContext(WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO);
/**
* Word information used to represent previous words information.
*/
public static class WordInfo {
+ @Nonnull
public static final WordInfo EMPTY_WORD_INFO = new WordInfo(null);
- public static final WordInfo BEGINNING_OF_SENTENCE = new WordInfo();
+ @Nonnull
+ public static final WordInfo BEGINNING_OF_SENTENCE_WORD_INFO = new WordInfo();
// This is an empty char sequence when mIsBeginningOfSentence is true.
public final CharSequence mWord;
@@ -47,7 +55,7 @@ public class PrevWordsInfo {
public final boolean mIsBeginningOfSentence;
// Beginning of sentence.
- public WordInfo() {
+ private WordInfo() {
mWord = "";
mIsBeginningOfSentence = true;
}
@@ -86,38 +94,55 @@ public class PrevWordsInfo {
// For simplicity of implementation, elements may also be EMPTY_WORD_INFO transiently after the
// WordComposer was reset and before starting a new composing word, but we should never be
// calling getSuggetions* in this situation.
- public WordInfo[] mPrevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
+ private final WordInfo[] mPrevWordsInfo;
+ private final int mPrevWordsCount;
// Construct from the previous word information.
- public PrevWordsInfo(final WordInfo prevWordInfo) {
- mPrevWordsInfo[0] = prevWordInfo;
- }
-
- // Construct from WordInfo array. n-th element represents (n+1)-th previous word's information.
- public PrevWordsInfo(final WordInfo[] prevWordsInfo) {
- for (int i = 0; i < Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM; i++) {
- mPrevWordsInfo[i] =
- (prevWordsInfo.length > i) ? prevWordsInfo[i] : WordInfo.EMPTY_WORD_INFO;
- }
+ public NgramContext(final WordInfo... prevWordsInfo) {
+ mPrevWordsInfo = prevWordsInfo;
+ mPrevWordsCount = prevWordsInfo.length;
}
// Create next prevWordsInfo using current prevWordsInfo.
- public PrevWordsInfo getNextPrevWordsInfo(final WordInfo wordInfo) {
- final WordInfo[] prevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
+ @Nonnull
+ public NgramContext getNextNgramContext(final WordInfo wordInfo) {
+ final int nextPrevWordCount = Math.min(Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM,
+ mPrevWordsCount + 1);
+ final WordInfo[] prevWordsInfo = new WordInfo[nextPrevWordCount];
prevWordsInfo[0] = wordInfo;
- for (int i = 1; i < prevWordsInfo.length; i++) {
- prevWordsInfo[i] = mPrevWordsInfo[i - 1];
- }
- return new PrevWordsInfo(prevWordsInfo);
+ System.arraycopy(mPrevWordsInfo, 0, prevWordsInfo, 1, nextPrevWordCount - 1);
+ return new NgramContext(prevWordsInfo);
}
public boolean isValid() {
- return mPrevWordsInfo[0].isValid();
+ return mPrevWordsCount > 0 && mPrevWordsInfo[0].isValid();
+ }
+
+ public boolean isBeginningOfSentenceContext() {
+ return mPrevWordsCount > 0 && mPrevWordsInfo[0].mIsBeginningOfSentence;
+ }
+
+ // n is 1-indexed.
+ // TODO: Remove
+ public CharSequence getNthPrevWord(final int n) {
+ if (n <= 0 || n > mPrevWordsCount) {
+ return null;
+ }
+ return mPrevWordsInfo[n - 1].mWord;
+ }
+
+ // n is 1-indexed.
+ @UsedForTesting
+ public boolean isNthPrevWordBeginningOfSontence(final int n) {
+ if (n <= 0 || n > mPrevWordsCount) {
+ return false;
+ }
+ return mPrevWordsInfo[n - 1].mIsBeginningOfSentence;
}
public void outputToArray(final int[][] codePointArrays,
final boolean[] isBeginningOfSentenceArray) {
- for (int i = 0; i < mPrevWordsInfo.length; i++) {
+ for (int i = 0; i < mPrevWordsCount; i++) {
final WordInfo wordInfo = mPrevWordsInfo[i];
if (wordInfo == null || !wordInfo.isValid()) {
codePointArrays[i] = new int[0];
@@ -129,28 +154,65 @@ public class PrevWordsInfo {
}
}
+ public int getPrevWordCount() {
+ return mPrevWordsCount;
+ }
+
@Override
public int hashCode() {
- return Arrays.hashCode(mPrevWordsInfo);
+ int hashValue = 0;
+ for (final WordInfo wordInfo : mPrevWordsInfo) {
+ if (wordInfo == null || !WordInfo.EMPTY_WORD_INFO.equals(wordInfo)) {
+ break;
+ }
+ hashValue ^= wordInfo.hashCode();
+ }
+ return hashValue;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
- if (!(o instanceof PrevWordsInfo)) return false;
- final PrevWordsInfo prevWordsInfo = (PrevWordsInfo)o;
- return Arrays.equals(mPrevWordsInfo, prevWordsInfo.mPrevWordsInfo);
+ if (!(o instanceof NgramContext)) return false;
+ final NgramContext prevWordsInfo = (NgramContext)o;
+
+ final int minLength = Math.min(mPrevWordsCount, prevWordsInfo.mPrevWordsCount);
+ for (int i = 0; i < minLength; i++) {
+ if (!mPrevWordsInfo[i].equals(prevWordsInfo.mPrevWordsInfo[i])) {
+ return false;
+ }
+ }
+ final WordInfo[] longerWordsInfo;
+ final int longerWordsInfoCount;
+ if (mPrevWordsCount > prevWordsInfo.mPrevWordsCount) {
+ longerWordsInfo = mPrevWordsInfo;
+ longerWordsInfoCount = mPrevWordsCount;
+ } else {
+ longerWordsInfo = prevWordsInfo.mPrevWordsInfo;
+ longerWordsInfoCount = prevWordsInfo.mPrevWordsCount;
+ }
+ for (int i = minLength; i < longerWordsInfoCount; i++) {
+ if (longerWordsInfo[i] != null
+ && !WordInfo.EMPTY_WORD_INFO.equals(longerWordsInfo[i])) {
+ return false;
+ }
+ }
+ return true;
}
@Override
public String toString() {
final StringBuffer builder = new StringBuffer();
- for (int i = 0; i < mPrevWordsInfo.length; i++) {
+ for (int i = 0; i < mPrevWordsCount; i++) {
final WordInfo wordInfo = mPrevWordsInfo[i];
builder.append("PrevWord[");
builder.append(i);
builder.append("]: ");
- if (wordInfo == null || !wordInfo.isValid()) {
+ if (wordInfo == null) {
+ builder.append("null. ");
+ continue;
+ }
+ if (!wordInfo.isValid()) {
builder.append("Empty. ");
continue;
}
diff --git a/java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java
new file mode 100644
index 000000000..2dbab0a3f
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2014 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.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import android.content.Context;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.latin.ExpandableBinaryDictionary.UpdateEntriesForInputEventsCallback;
+import com.android.inputmethod.latin.personalization.PersonalizationDataChunk;
+import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
+import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
+import com.android.inputmethod.latin.utils.DistracterFilter;
+import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
+import com.android.inputmethod.latin.utils.WordInputEventForPersonalization;
+
+/**
+ * Class for managing and updating personalization dictionaries.
+ */
+public class PersonalizationHelperForDictionaryFacilitator {
+ private final Context mContext;
+ private final DistracterFilter mDistracterFilter;
+ private final HashMap<String, HashSet<Locale>> mLangToLocalesMap = new HashMap<>();
+ private final HashMap<Locale, ExpandableBinaryDictionary> mPersonalizationDictsToUpdate =
+ new HashMap<>();
+ private boolean mIsMonolingualUser = false;
+
+ PersonalizationHelperForDictionaryFacilitator(final Context context,
+ final DistracterFilter distracterFilter) {
+ mContext = context;
+ mDistracterFilter = distracterFilter;
+ }
+
+ public void close() {
+ mLangToLocalesMap.clear();
+ for (final ExpandableBinaryDictionary dict : mPersonalizationDictsToUpdate.values()) {
+ dict.close();
+ }
+ mPersonalizationDictsToUpdate.clear();
+ }
+
+ public void clearDictionariesToUpdate() {
+ for (final ExpandableBinaryDictionary dict : mPersonalizationDictsToUpdate.values()) {
+ dict.clear();
+ }
+ mPersonalizationDictsToUpdate.clear();
+ }
+
+ public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) {
+ for (final InputMethodSubtype subtype : enabledSubtypes) {
+ final Locale locale = SubtypeLocaleUtils.getSubtypeLocale(subtype);
+ final String language = locale.getLanguage();
+ final HashSet<Locale> locales = mLangToLocalesMap.get(language);
+ if (locales != null) {
+ locales.add(locale);
+ } else {
+ final HashSet<Locale> localeSet = new HashSet<>();
+ localeSet.add(locale);
+ mLangToLocalesMap.put(language, localeSet);
+ }
+ }
+ }
+
+ public void setIsMonolingualUser(final boolean isMonolingualUser) {
+ mIsMonolingualUser = isMonolingualUser;
+ }
+
+ /**
+ * Flush personalization dictionaries to dictionary files. Close dictionaries after writing
+ * files except the dictionaries that is used for generating suggestions.
+ *
+ * @param personalizationDictsUsedForSuggestion the personalization dictionaries used for
+ * generating suggestions that won't be closed.
+ */
+ public void flushPersonalizationDictionariesToUpdate(
+ final HashSet<ExpandableBinaryDictionary> personalizationDictsUsedForSuggestion) {
+ for (final ExpandableBinaryDictionary personalizationDict :
+ mPersonalizationDictsToUpdate.values()) {
+ personalizationDict.asyncFlushBinaryDictionary();
+ if (!personalizationDictsUsedForSuggestion.contains(personalizationDict)) {
+ // Close if the dictionary is not being used for suggestion.
+ personalizationDict.close();
+ }
+ }
+ mDistracterFilter.close();
+ mPersonalizationDictsToUpdate.clear();
+ }
+
+ private ExpandableBinaryDictionary getPersonalizationDictToUpdate(final Context context,
+ final Locale locale) {
+ ExpandableBinaryDictionary personalizationDict = mPersonalizationDictsToUpdate.get(locale);
+ if (personalizationDict != null) {
+ return personalizationDict;
+ }
+ personalizationDict = PersonalizationDictionary.getDictionary(context, locale,
+ null /* dictFile */, "" /* dictNamePrefix */);
+ mPersonalizationDictsToUpdate.put(locale, personalizationDict);
+ return personalizationDict;
+ }
+
+ private void updateEntriesOfPersonalizationDictionariesForLocale(final Locale locale,
+ final PersonalizationDataChunk personalizationDataChunk,
+ final SpacingAndPunctuations spacingAndPunctuations,
+ final UpdateEntriesForInputEventsCallback callback) {
+ final ExpandableBinaryDictionary personalizationDict =
+ getPersonalizationDictToUpdate(mContext, locale);
+ if (personalizationDict == null) {
+ if (callback != null) {
+ callback.onFinished();
+ }
+ return;
+ }
+ final ArrayList<WordInputEventForPersonalization> inputEvents =
+ WordInputEventForPersonalization.createInputEventFrom(
+ personalizationDataChunk.mTokens,
+ personalizationDataChunk.mTimestampInSeconds, spacingAndPunctuations,
+ locale, new DistracterFilterCheckingIsInDictionary(
+ mDistracterFilter, personalizationDict));
+ if (inputEvents == null || inputEvents.isEmpty()) {
+ if (callback != null) {
+ callback.onFinished();
+ }
+ return;
+ }
+ personalizationDict.updateEntriesForInputEvents(inputEvents, callback);
+ }
+
+ public void updateEntriesOfPersonalizationDictionaries(final Locale defaultLocale,
+ final PersonalizationDataChunk personalizationDataChunk,
+ final SpacingAndPunctuations spacingAndPunctuations,
+ final UpdateEntriesForInputEventsCallback callback) {
+ final String language = personalizationDataChunk.mDetectedLanguage;
+ final HashSet<Locale> locales;
+ if (mIsMonolingualUser && PersonalizationDataChunk.LANGUAGE_UNKNOWN.equals(language)
+ && mLangToLocalesMap.size() == 1) {
+ locales = mLangToLocalesMap.get(defaultLocale.getLanguage());
+ } else {
+ locales = mLangToLocalesMap.get(language);
+ }
+ if (locales == null || locales.isEmpty()) {
+ if (callback != null) {
+ callback.onFinished();
+ }
+ return;
+ }
+ final AtomicInteger remainingTaskCount = new AtomicInteger(locales.size());
+ final UpdateEntriesForInputEventsCallback callbackForLocales =
+ new UpdateEntriesForInputEventsCallback() {
+ @Override
+ public void onFinished() {
+ if (remainingTaskCount.decrementAndGet() == 0) {
+ // Update tasks for all locales have been finished.
+ if (callback != null) {
+ callback.onFinished();
+ }
+ }
+ }
+ };
+ for (final Locale locale : locales) {
+ updateEntriesOfPersonalizationDictionariesForLocale(locale, personalizationDataChunk,
+ spacingAndPunctuations, callbackForLocales);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java
index 56014cbad..a65304cd0 100644
--- a/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java
@@ -17,7 +17,8 @@
package com.android.inputmethod.latin;
import com.android.inputmethod.keyboard.internal.KeySpecParser;
-import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
@@ -56,7 +57,7 @@ public final class PunctuationSuggestions extends SuggestedWords {
/**
* {@inheritDoc}
- * Note that {@link super#getWord(int)} returns a punctuation key specification text.
+ * Note that {@link SuggestedWords#getWord(int)} returns a punctuation key specification text.
* The suggested punctuation should be gotten by parsing the key specification.
*/
@Override
@@ -70,7 +71,7 @@ public final class PunctuationSuggestions extends SuggestedWords {
/**
* {@inheritDoc}
- * Note that {@link super#getWord(int)} returns a punctuation key specification text.
+ * Note that {@link SuggestedWords#getWord(int)} returns a punctuation key specification text.
* The displayed text should be gotten by parsing the key specification.
*/
@Override
@@ -82,7 +83,7 @@ public final class PunctuationSuggestions extends SuggestedWords {
/**
* {@inheritDoc}
* Note that {@link #getWord(int)} returns a suggested punctuation. We should create a
- * {@link SuggestedWordInfo} object that represents a hard coded word.
+ * {@link SuggestedWords.SuggestedWordInfo} object that represents a hard coded word.
*/
@Override
public SuggestedWordInfo getInfo(final int index) {
diff --git a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java
index 5d4fc5861..7b1a53a6e 100644
--- a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java
@@ -16,8 +16,8 @@
package com.android.inputmethod.latin;
-import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
import java.util.ArrayList;
@@ -40,7 +40,7 @@ public final class ReadOnlyBinaryDictionary extends Dictionary {
public ReadOnlyBinaryDictionary(final String filename, final long offset, final long length,
final boolean useFullEditDistance, final Locale locale, final String dictType) {
- super(dictType);
+ super(dictType, locale);
mBinaryDictionary = new BinaryDictionary(filename, offset, length, useFullEditDistance,
locale, dictType, false /* isUpdatable */);
}
@@ -50,14 +50,16 @@ public final class ReadOnlyBinaryDictionary extends Dictionary {
}
@Override
- public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
+ public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+ final NgramContext ngramContext, final long proximityInfoHandle,
final SettingsValuesForSuggestion settingsValuesForSuggestion,
- final int sessionId, final float[] inOutLanguageWeight) {
+ final int sessionId, final float weightForLocale,
+ final float[] inOutWeightOfLangModelVsSpatialModel) {
if (mLock.readLock().tryLock()) {
try {
- return mBinaryDictionary.getSuggestions(composer, prevWordsInfo, proximityInfo,
- settingsValuesForSuggestion, sessionId, inOutLanguageWeight);
+ return mBinaryDictionary.getSuggestions(composedData, ngramContext,
+ proximityInfoHandle, settingsValuesForSuggestion, sessionId,
+ weightForLocale, inOutWeightOfLangModelVsSpatialModel);
} finally {
mLock.readLock().unlock();
}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 744b0321a..834f747d9 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -19,6 +19,7 @@ package com.android.inputmethod.latin;
import android.graphics.Color;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
+import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
@@ -33,16 +34,18 @@ import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import com.android.inputmethod.compat.InputConnectionCompatUtils;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
+import com.android.inputmethod.latin.inputlogic.PrivateCommandPerformer;
import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
import com.android.inputmethod.latin.utils.CapsModeUtils;
import com.android.inputmethod.latin.utils.DebugLogUtils;
-import com.android.inputmethod.latin.utils.PrevWordsInfoUtils;
+import com.android.inputmethod.latin.utils.NgramContextUtils;
import com.android.inputmethod.latin.utils.ScriptUtils;
import com.android.inputmethod.latin.utils.SpannableStringUtils;
-import com.android.inputmethod.latin.utils.StringUtils;
import com.android.inputmethod.latin.utils.TextRange;
-import java.util.Arrays;
+import javax.annotation.Nonnull;
/**
* Enrichment class for InputConnection to simplify interaction and add functionality.
@@ -52,7 +55,7 @@ import java.util.Arrays;
* all the time to find out what text is in the buffer, when we need it to determine caps mode
* for example.
*/
-public final class RichInputConnection {
+public final class RichInputConnection implements PrivateCommandPerformer {
private static final String TAG = RichInputConnection.class.getSimpleName();
private static final boolean DBG = false;
private static final boolean DEBUG_PREVIOUS_TEXT = false;
@@ -89,7 +92,7 @@ public final class RichInputConnection {
/**
* This variable is a temporary object used in
- * {@link #commitTextWithBackgroundColor(CharSequence, int, int)} to avoid object creation.
+ * {@link #commitTextWithBackgroundColor(CharSequence,int,int,int)} to avoid object creation.
*/
private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder();
/**
@@ -149,9 +152,8 @@ public final class RichInputConnection {
} else {
if (DBG) {
throw new RuntimeException("Nest level too deep");
- } else {
- Log.e(TAG, "Nest level too deep : " + mNestLevel);
}
+ Log.e(TAG, "Nest level too deep : " + mNestLevel);
}
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
@@ -349,10 +351,9 @@ public final class RichInputConnection {
// If we have some composing text and a space before, then we should have
// MODE_CHARACTERS and MODE_WORDS on.
return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType;
- } else {
- // We have some composing text - we should be in MODE_CHARACTERS only.
- return TextUtils.CAP_MODE_CHARACTERS & inputType;
}
+ // We have some composing text - we should be in MODE_CHARACTERS only.
+ return TextUtils.CAP_MODE_CHARACTERS & inputType;
}
// TODO: this will generally work, but there may be cases where the buffer contains SOME
// information but not enough to determine the caps mode accurately. This may happen after
@@ -367,7 +368,9 @@ public final class RichInputConnection {
}
// This never calls InputConnection#getCapsMode - in fact, it's a static method that
// never blocks or initiates IPC.
- return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType,
+ // TODO: don't call #toString() here. Instead, all accesses to
+ // mCommittedTextBeforeComposingText should be done on the main thread.
+ return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText.toString(), inputType,
spacingAndPunctuations, hasSpaceBefore);
}
@@ -593,11 +596,12 @@ public final class RichInputConnection {
}
@SuppressWarnings("unused")
- public PrevWordsInfo getPrevWordsInfoFromNthPreviousWord(
+ @Nonnull
+ public NgramContext getNgramContextFromNthPreviousWord(
final SpacingAndPunctuations spacingAndPunctuations, final int n) {
mIC = mParent.getCurrentInputConnection();
if (null == mIC) {
- return PrevWordsInfo.EMPTY_PREV_WORDS_INFO;
+ return NgramContext.EMPTY_PREV_WORDS_INFO;
}
final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
if (DEBUG_PREVIOUS_TEXT && null != prev) {
@@ -618,14 +622,10 @@ public final class RichInputConnection {
}
}
}
- return PrevWordsInfoUtils.getPrevWordsInfoFromNthPreviousWord(
+ return NgramContextUtils.getNgramContextFromNthPreviousWord(
prev, spacingAndPunctuations, n);
}
- private static boolean isSeparator(final int code, final int[] sortedSeparators) {
- return Arrays.binarySearch(sortedSeparators, code) >= 0;
- }
-
private static boolean isPartOfCompositionForScript(final int codePoint,
final SpacingAndPunctuations spacingAndPunctuations, final int scriptId) {
// We always consider word connectors part of compositions.
@@ -740,17 +740,19 @@ public final class RichInputConnection {
return TextUtils.equals(text, beforeText);
}
- public boolean revertDoubleSpacePeriod() {
+ public boolean revertDoubleSpacePeriod(final SpacingAndPunctuations spacingAndPunctuations) {
if (DEBUG_BATCH_NESTING) 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 (!TextUtils.equals(Constants.STRING_PERIOD_AND_SPACE, textBeforeCursor)) {
+ if (!TextUtils.equals(spacingAndPunctuations.mSentenceSeparatorAndSpace,
+ 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 "
- + "\"" + Constants.STRING_PERIOD_AND_SPACE + "\" just before the cursor.");
+ Log.d(TAG, "Tried to revert double-space combo but we didn't find \""
+ + spacingAndPunctuations.mSentenceSeparatorAndSpace
+ + "\" just before the cursor.");
return false;
}
// Double-space results in ". ". A backspace to cancel this should result in a single
@@ -847,17 +849,32 @@ public final class RichInputConnection {
/**
* Try to get the text from the editor to expose lies the framework may have been
- * telling us. Concretely, when the device rotates, the frameworks tells us about where the
- * cursor used to be initially in the editor at the time it first received the focus; this
+ * telling us. Concretely, when the device rotates and when the keyboard reopens in the same
+ * text field after having been closed with the back key, the frameworks tells us about where
+ * the cursor used to be initially in the editor at the time it first received the focus; this
* may be completely different from the place it is upon rotation. Since we don't have any
* means to get the real value, try at least to ask the text view for some characters and
* detect the most damaging cases: when the cursor position is declared to be much smaller
* than it really is.
*/
public void tryFixLyingCursorPosition() {
+ mIC = mParent.getCurrentInputConnection();
final CharSequence textBeforeCursor = getTextBeforeCursor(
Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
- if (null == textBeforeCursor) {
+ final CharSequence selectedText = null == mIC ? null : mIC.getSelectedText(0 /* flags */);
+ if (null == textBeforeCursor ||
+ (!TextUtils.isEmpty(selectedText) && mExpectedSelEnd == mExpectedSelStart)) {
+ // If textBeforeCursor is null, we have no idea what kind of text field we have or if
+ // thinking about the "cursor position" actually makes any sense. In this case we
+ // remember a meaningless cursor position. Contrast this with an empty string, which is
+ // valid and should mean the cursor is at the start of the text.
+ // Also, if we expect we don't have a selection but we DO have non-empty selected text,
+ // then the framework lied to us about the cursor position. In this case, we should just
+ // revert to the most basic behavior possible for the next action (backspace in
+ // particular comes to mind), so we remember a meaningless cursor position which should
+ // result in degraded behavior from the next input.
+ // Interestingly, in either case, chances are any action the user takes next will result
+ // in a call to onUpdateSelection, which should set things right.
mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION;
} else {
final int textLength = textBeforeCursor.length();
@@ -880,6 +897,15 @@ public final class RichInputConnection {
}
}
+ @Override
+ public boolean performPrivateCommand(final String action, final Bundle data) {
+ mIC = mParent.getCurrentInputConnection();
+ if (mIC == null) {
+ return false;
+ }
+ return mIC.performPrivateCommand(action, data);
+ }
+
public int getExpectedSelectionStart() {
return mExpectedSelStart;
}
@@ -949,7 +975,8 @@ public final class RichInputConnection {
/**
* @return {@code true} if the application reported that the monitor mode of
- * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is currently enabled.
+ * {@link InputMethodService#onUpdateCursorAnchorInfo(android.view.inputmethod.CursorAnchorInfo)}
+ * is currently enabled.
*/
public boolean isCursorAnchorInfoMonitorEnabled() {
return mCursorAnchorInfoMonitorEnabled;
diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
index 7cf4eff92..8d8e7ac38 100644
--- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
+++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
@@ -16,7 +16,7 @@
package com.android.inputmethod.latin;
-import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE;
+import static com.android.inputmethod.latin.common.Constants.Subtype.KEYBOARD_MODE;
import android.content.Context;
import android.content.SharedPreferences;
@@ -29,6 +29,7 @@ import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.latin.settings.AdditionalFeaturesSettingUtils;
import com.android.inputmethod.latin.settings.Settings;
import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
@@ -37,10 +38,13 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import javax.annotation.Nonnull;
+
/**
* Enrichment class for InputMethodManager to simplify interaction and add functionality.
*/
-public final class RichInputMethodManager {
+// non final for easy mocking.
+public class RichInputMethodManager {
private static final String TAG = RichInputMethodManager.class.getSimpleName();
private RichInputMethodManager() {
@@ -49,6 +53,7 @@ public final class RichInputMethodManager {
private static final RichInputMethodManager sInstance = new RichInputMethodManager();
+ private Context mContext;
private InputMethodManagerCompatWrapper mImmWrapper;
private InputMethodInfoCache mInputMethodInfoCache;
final HashMap<InputMethodInfo, List<InputMethodSubtype>>
@@ -82,6 +87,7 @@ public final class RichInputMethodManager {
return;
}
mImmWrapper = new InputMethodManagerCompatWrapper(context);
+ mContext = context;
mInputMethodInfoCache = new InputMethodInfoCache(
mImmWrapper.mImm, context.getPackageName());
@@ -297,10 +303,15 @@ public final class RichInputMethodManager {
return INDEX_NOT_FOUND;
}
- public InputMethodSubtype getCurrentInputMethodSubtype(
- final InputMethodSubtype defaultSubtype) {
- final InputMethodSubtype currentSubtype = mImmWrapper.mImm.getCurrentInputMethodSubtype();
- return (currentSubtype != null) ? currentSubtype : defaultSubtype;
+ @Nonnull
+ public InputMethodSubtype getCurrentRawSubtype() {
+ return mImmWrapper.mImm.getCurrentInputMethodSubtype();
+ }
+
+ public RichInputMethodSubtype createCurrentRichInputMethodSubtype(
+ @Nonnull final InputMethodSubtype rawSubtype) {
+ return AdditionalFeaturesSettingUtils.createRichInputMethodSubtype(this, rawSubtype,
+ mContext);
}
public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) {
diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java
new file mode 100644
index 000000000..8d055531c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2014 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.InputMethodSubtype;
+
+import com.android.inputmethod.latin.utils.LocaleUtils;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Enrichment class for InputMethodSubtype to enable concurrent multi-lingual input.
+ *
+ * Right now, this returns the extra value of its primary subtype.
+ */
+public final class RichInputMethodSubtype {
+ private final InputMethodSubtype mSubtype;
+ private final Locale[] mLocales;
+
+ public RichInputMethodSubtype(final InputMethodSubtype subtype, final Locale... locales) {
+ mSubtype = subtype;
+ mLocales = new Locale[locales.length+1];
+ mLocales[0] = LocaleUtils.constructLocaleFromString(mSubtype.getLocale());
+ System.arraycopy(locales, 0, mLocales, 1, locales.length);
+ }
+
+ // Extra values are determined by the primary subtype. This is probably right, but
+ // we may have to revisit this later.
+ public String getExtraValueOf(final String key) {
+ return mSubtype.getExtraValueOf(key);
+ }
+
+ // The mode is also determined by the primary subtype.
+ public String getMode() {
+ return mSubtype.getMode();
+ }
+
+ public boolean isNoLanguage() {
+ if (mLocales.length > 1) {
+ return false;
+ }
+ return SubtypeLocaleUtils.NO_LANGUAGE.equals(mSubtype.getLocale());
+ }
+
+ public String getNameForLogging() {
+ return toString();
+ }
+
+ // InputMethodSubtype's display name for spacebar text in its locale.
+ // isAdditionalSubtype (T=true, F=false)
+ // locale layout | Middle Full
+ // ------ ------- - --------- ----------------------
+ // en_US qwerty F English English (US) exception
+ // en_GB qwerty F English English (UK) exception
+ // es_US spanish F Español Español (EE.UU.) exception
+ // fr azerty F Français Français
+ // fr_CA qwerty F Français Français (Canada)
+ // fr_CH swiss F Français Français (Suisse)
+ // de qwertz F Deutsch Deutsch
+ // de_CH swiss T Deutsch Deutsch (Schweiz)
+ // zz qwerty F QWERTY QWERTY
+ // fr qwertz T Français Français
+ // de qwerty T Deutsch Deutsch
+ // en_US azerty T English English (US)
+ // zz azerty T AZERTY AZERTY
+ // Get the RichInputMethodSubtype's full display name in its locale.
+ public String getFullDisplayName() {
+ if (isNoLanguage()) {
+ return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype);
+ }
+ return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(mSubtype.getLocale());
+ }
+
+ // Get the RichInputMethodSubtype's middle display name in its locale.
+ public String getMiddleDisplayName() {
+ if (isNoLanguage()) {
+ return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype);
+ }
+ return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(mSubtype.getLocale());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof RichInputMethodSubtype)) {
+ return false;
+ }
+ final RichInputMethodSubtype other = (RichInputMethodSubtype)o;
+ return mSubtype.equals(other.mSubtype) && Arrays.equals(mLocales, other.mLocales);
+ }
+
+ @Override
+ public int hashCode() {
+ return mSubtype.hashCode() + Arrays.hashCode(mLocales);
+ }
+
+ @Override
+ public String toString() {
+ return "Multi-lingual subtype: " + mSubtype.toString() + ", " + Arrays.toString(mLocales);
+ }
+
+ public Locale[] getLocales() {
+ return mLocales;
+ }
+
+ public boolean isRtlSubtype() {
+ // The subtype is considered RTL if the language of the main subtype is RTL.
+ return SubtypeLocaleUtils.isRtlLanguage(mLocales[0]);
+ }
+
+ // TODO: remove this method
+ public InputMethodSubtype getRawSubtype() { return mSubtype; }
+}
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index a725e1611..98bce95bd 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -16,8 +16,7 @@
package com.android.inputmethod.latin;
-import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.REQ_NETWORK_CONNECTIVITY;
-
+import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.REQ_NETWORK_CONNECTIVITY;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
@@ -35,8 +34,8 @@ import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.keyboard.internal.LanguageOnSpacebarHelper;
+import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.define.DebugFlags;
-import com.android.inputmethod.latin.utils.LocaleUtils;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
import java.util.HashSet;
@@ -45,6 +44,8 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import javax.annotation.Nonnull;
+
public final class SubtypeSwitcher {
private static boolean DBG = DebugFlags.DEBUG_ENABLED;
private static final String TAG = SubtypeSwitcher.class.getSimpleName();
@@ -58,8 +59,9 @@ public final class SubtypeSwitcher {
new LanguageOnSpacebarHelper();
private InputMethodInfo mShortcutInputMethodInfo;
private InputMethodSubtype mShortcutSubtype;
- private InputMethodSubtype mNoLanguageSubtype;
- private InputMethodSubtype mEmojiSubtype;
+ private RichInputMethodSubtype mCurrentRichInputMethodSubtype;
+ private RichInputMethodSubtype mNoLanguageSubtype;
+ private RichInputMethodSubtype mEmojiSubtype;
private boolean mIsNetworkConnected;
private static final String KEYBOARD_MODE = "keyboard";
@@ -70,26 +72,26 @@ public final class SubtypeSwitcher {
+ "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
+ "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE
+ "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
- private static final InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE =
- InputMethodSubtypeCompatUtils.newInputMethodSubtype(
+ private static final RichInputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE =
+ new RichInputMethodSubtype(InputMethodSubtypeCompatUtils.newInputMethodSubtype(
R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark,
SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE,
false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
- SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE);
+ SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE));
// Caveat: We probably should remove this when we add an Emoji subtype in {@link R.xml.method}.
// Dummy Emoji subtype. See {@link R.xml.method}.
private static final int SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE = 0xd78b2ed0;
private static final String EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE =
"KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI
+ "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
- private static final InputMethodSubtype DUMMY_EMOJI_SUBTYPE =
+ private static final RichInputMethodSubtype DUMMY_EMOJI_SUBTYPE = new RichInputMethodSubtype(
InputMethodSubtypeCompatUtils.newInputMethodSubtype(
R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark,
SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE,
false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
- SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE);
+ SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE));
public static SubtypeSwitcher getInstance() {
return sInstance;
@@ -117,10 +119,14 @@ public final class SubtypeSwitcher {
final NetworkInfo info = connectivityManager.getActiveNetworkInfo();
mIsNetworkConnected = (info != null && info.isConnected());
- onSubtypeChanged(getCurrentSubtype());
+ refreshSubtypeInfo();
updateParametersOnStartInputView();
}
+ public void refreshSubtypeInfo() {
+ onSubtypeChanged(mRichImm.getCurrentRawSubtype());
+ }
+
/**
* Update parameters which are changed outside LatinIME. This parameters affect UI so that they
* should be updated every time onStartInputView is called.
@@ -165,21 +171,28 @@ public final class SubtypeSwitcher {
}
// Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function.
- public void onSubtypeChanged(final InputMethodSubtype newSubtype) {
+ public void onSubtypeChanged(@Nonnull final InputMethodSubtype newSubtype) {
+ final RichInputMethodSubtype richSubtype =
+ mRichImm.createCurrentRichInputMethodSubtype(newSubtype);
if (DBG) {
- Log.w(TAG, "onSubtypeChanged: "
- + SubtypeLocaleUtils.getSubtypeNameForLogging(newSubtype));
+ Log.w(TAG, "onSubtypeChanged: " + richSubtype.getNameForLogging());
+ }
+ mCurrentRichInputMethodSubtype = richSubtype;
+ final Locale[] newLocales = richSubtype.getLocales();
+ if (newLocales.length > 1) {
+ // In multi-locales mode, the system language is never the same as the input language
+ // because there is no single input language.
+ mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(false);
+ } else {
+ final Locale newLocale = newLocales[0];
+ final Locale systemLocale = mResources.getConfiguration().locale;
+ final boolean sameLocale = systemLocale.equals(newLocale);
+ final boolean sameLanguage = systemLocale.getLanguage().equals(newLocale.getLanguage());
+ final boolean implicitlyEnabled = mRichImm
+ .checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype);
+ mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(
+ sameLocale || (sameLanguage && implicitlyEnabled));
}
-
- final Locale newLocale = SubtypeLocaleUtils.getSubtypeLocale(newSubtype);
- final Locale systemLocale = mResources.getConfiguration().locale;
- final boolean sameLocale = systemLocale.equals(newLocale);
- final boolean sameLanguage = systemLocale.getLanguage().equals(newLocale.getLanguage());
- final boolean implicitlyEnabled =
- mRichImm.checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype);
- mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(
- sameLocale || (sameLanguage && implicitlyEnabled));
-
updateShortcutIME();
}
@@ -250,7 +263,7 @@ public final class SubtypeSwitcher {
// Subtype Switching functions //
//////////////////////////////////
- public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) {
+ public int getLanguageOnSpacebarFormatType(final RichInputMethodSubtype subtype) {
return mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(subtype);
}
@@ -279,30 +292,32 @@ public final class SubtypeSwitcher {
return true;
}
- private static InputMethodSubtype sForcedSubtypeForTesting = null;
+ private static RichInputMethodSubtype sForcedSubtypeForTesting = null;
+
@UsedForTesting
- void forceSubtype(final InputMethodSubtype subtype) {
- sForcedSubtypeForTesting = subtype;
+ static void forceSubtype(final InputMethodSubtype subtype) {
+ sForcedSubtypeForTesting = new RichInputMethodSubtype(subtype);
}
- public Locale getCurrentSubtypeLocale() {
+ public Locale[] getCurrentSubtypeLocales() {
if (null != sForcedSubtypeForTesting) {
- return LocaleUtils.constructLocaleFromString(sForcedSubtypeForTesting.getLocale());
+ return sForcedSubtypeForTesting.getLocales();
}
- return SubtypeLocaleUtils.getSubtypeLocale(getCurrentSubtype());
+ return getCurrentSubtype().getLocales();
}
- public InputMethodSubtype getCurrentSubtype() {
+ public RichInputMethodSubtype getCurrentSubtype() {
if (null != sForcedSubtypeForTesting) {
return sForcedSubtypeForTesting;
}
- return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype());
+ return mCurrentRichInputMethodSubtype;
}
- public InputMethodSubtype getNoLanguageSubtype() {
+ public RichInputMethodSubtype getNoLanguageSubtype() {
if (mNoLanguageSubtype == null) {
- mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
- SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
+ mNoLanguageSubtype = new RichInputMethodSubtype(
+ mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+ SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY));
}
if (mNoLanguageSubtype != null) {
return mNoLanguageSubtype;
@@ -313,10 +328,14 @@ public final class SubtypeSwitcher {
return DUMMY_NO_LANGUAGE_SUBTYPE;
}
- public InputMethodSubtype getEmojiSubtype() {
+ public RichInputMethodSubtype getEmojiSubtype() {
if (mEmojiSubtype == null) {
- mEmojiSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
- SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
+ final InputMethodSubtype rawEmojiSubtype =
+ mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+ SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
+ if (null != rawEmojiSubtype) {
+ mEmojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype);
+ }
}
if (mEmojiSubtype != null) {
return mEmojiSubtype;
@@ -328,6 +347,6 @@ public final class SubtypeSwitcher {
}
public String getCombiningRulesExtraValueOfCurrentSubtype() {
- return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype());
+ return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype());
}
}
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index b03818c1d..9b4619d35 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -20,14 +20,16 @@ import android.text.TextUtils;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.define.DebugFlags;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
-import com.android.inputmethod.latin.utils.StringUtils;
import com.android.inputmethod.latin.utils.SuggestionResults;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.Locale;
/**
@@ -49,16 +51,22 @@ public final class Suggest {
private static final boolean DBG = DebugFlags.DEBUG_ENABLED;
private final DictionaryFacilitator mDictionaryFacilitator;
+ private static final int MAXIMUM_AUTO_CORRECT_LENGTH_FOR_GERMAN = 12;
+ private static final HashMap<String, Integer> sLanguageToMaximumAutoCorrectionWithSpaceLength =
+ new HashMap<>();
+ static {
+ // TODO: should we add Finnish here?
+ // TODO: This should not be hardcoded here but be written in the dictionary header
+ sLanguageToMaximumAutoCorrectionWithSpaceLength.put(Locale.GERMAN.getLanguage(),
+ MAXIMUM_AUTO_CORRECT_LENGTH_FOR_GERMAN);
+ }
+
private float mAutoCorrectionThreshold;
public Suggest(final DictionaryFacilitator dictionaryFacilitator) {
mDictionaryFacilitator = dictionaryFacilitator;
}
- public Locale getLocale() {
- return mDictionaryFacilitator.getLocale();
- }
-
public void setAutoCorrectionThreshold(final float threshold) {
mAutoCorrectionThreshold = threshold;
}
@@ -68,15 +76,15 @@ public final class Suggest {
}
public void getSuggestedWords(final WordComposer wordComposer,
- final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
+ final NgramContext ngramContext, final ProximityInfo proximityInfo,
final SettingsValuesForSuggestion settingsValuesForSuggestion,
final boolean isCorrectionEnabled, final int inputStyle, final int sequenceNumber,
final OnGetSuggestedWordsCallback callback) {
if (wordComposer.isBatchMode()) {
- getSuggestedWordsForBatchInput(wordComposer, prevWordsInfo, proximityInfo,
+ getSuggestedWordsForBatchInput(wordComposer, ngramContext, proximityInfo,
settingsValuesForSuggestion, inputStyle, sequenceNumber, callback);
} else {
- getSuggestedWordsForNonBatchInput(wordComposer, prevWordsInfo, proximityInfo,
+ getSuggestedWordsForNonBatchInput(wordComposer, ngramContext, proximityInfo,
settingsValuesForSuggestion, inputStyle, isCorrectionEnabled,
sequenceNumber, callback);
}
@@ -84,7 +92,7 @@ public final class Suggest {
private static ArrayList<SuggestedWordInfo> getTransformedSuggestedWordInfoList(
final WordComposer wordComposer, final SuggestionResults results,
- final int trailingSingleQuotesCount) {
+ final int trailingSingleQuotesCount, final Locale defaultLocale) {
final boolean shouldMakeSuggestionsAllUpperCase = wordComposer.isAllUpperCase()
&& !wordComposer.isResumed();
final boolean isOnlyFirstCharCapitalized =
@@ -96,9 +104,11 @@ public final class Suggest {
|| 0 != trailingSingleQuotesCount) {
for (int i = 0; i < suggestionsCount; ++i) {
final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
+ final Locale wordLocale = wordInfo.mSourceDict.mLocale;
final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
- wordInfo, results.mLocale, shouldMakeSuggestionsAllUpperCase,
- isOnlyFirstCharCapitalized, trailingSingleQuotesCount);
+ wordInfo, null == wordLocale ? defaultLocale : wordLocale,
+ shouldMakeSuggestionsAllUpperCase, isOnlyFirstCharCapitalized,
+ trailingSingleQuotesCount);
suggestionsContainer.set(i, transformedWordInfo);
}
}
@@ -119,7 +129,7 @@ public final class Suggest {
// Retrieves suggestions for non-batch input (typing, recorrection, predictions...)
// and calls the callback function with the suggestions.
private void getSuggestedWordsForNonBatchInput(final WordComposer wordComposer,
- final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
+ final NgramContext ngramContext, final ProximityInfo proximityInfo,
final SettingsValuesForSuggestion settingsValuesForSuggestion,
final int inputStyleIfNotPrediction, final boolean isCorrectionEnabled,
final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
@@ -130,11 +140,14 @@ public final class Suggest {
: typedWord;
final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
- wordComposer, prevWordsInfo, proximityInfo, settingsValuesForSuggestion,
- SESSION_ID_TYPING);
+ wordComposer, ngramContext, proximityInfo.getNativeProximityInfo(),
+ settingsValuesForSuggestion, SESSION_ID_TYPING);
final ArrayList<SuggestedWordInfo> suggestionsContainer =
getTransformedSuggestedWordInfoList(wordComposer, suggestionResults,
- trailingSingleQuotesCount);
+ trailingSingleQuotesCount,
+ // For transforming suggestions that don't come for any dictionary, we
+ // use the currently most probable locale as it's our best bet.
+ mDictionaryFacilitator.getMostProbableLocale());
final boolean didRemoveTypedWord =
SuggestedWordInfo.removeDups(wordComposer.getTypedWord(), suggestionsContainer);
@@ -147,28 +160,50 @@ public final class Suggest {
|| (consideredWord.length() > 1 && !didRemoveTypedWord);
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 || resultsArePredictions
- || suggestionResults.isEmpty() || wordComposer.hasDigits()
- || wordComposer.isMostlyCaps() || wordComposer.isResumed()
- || !mDictionaryFacilitator.hasInitializedMainDictionary()
+ // If correction is not enabled, we never auto-correct. This is for example for when
+ // the setting "Auto-correction" is "off": we still suggest, but we don't auto-correct.
+ if (!isCorrectionEnabled
+ // If the word does not allow to be auto-corrected, then we don't auto-correct.
+ || !allowsToBeAutoCorrected
+ // If we are doing prediction, then we never auto-correct of course
+ || resultsArePredictions
+ // If we don't have suggestion results, we can't evaluate the first suggestion
+ // for auto-correction
+ || suggestionResults.isEmpty()
+ // If the word has digits, we never auto-correct because it's likely the word
+ // was type with a lot of care
+ || wordComposer.hasDigits()
+ // If the word is mostly caps, we never auto-correct because this is almost
+ // certainly intentional (and careful input)
+ || wordComposer.isMostlyCaps()
+ // We never auto-correct when suggestions are resumed because it would be unexpected
+ || wordComposer.isResumed()
+ // 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. Also, it would probably get obnoxious quickly.
+ // TODO: now that we have personalization, we may want to re-evaluate this decision
+ || !mDictionaryFacilitator.hasAtLeastOneInitializedMainDictionary()
+ // If the first suggestion is a shortcut we never auto-correct to it, regardless
+ // of how strong it is (whitelist entries are not KIND_SHORTCUT but KIND_WHITELIST).
+ // TODO: we may want to have shortcut-only entries auto-correct in the future.
|| suggestionResults.first().isKindOf(SuggestedWordInfo.KIND_SHORTCUT)) {
- // 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.
- // Also, shortcuts should never auto-correct unless they are whitelist entries.
- // TODO: we may want to have shortcut-only entries auto-correct in the future.
hasAutoCorrection = false;
} else {
- hasAutoCorrection = AutoCorrectionUtils.suggestionExceedsAutoCorrectionThreshold(
- suggestionResults.first(), consideredWord, mAutoCorrectionThreshold);
+ final SuggestedWordInfo firstSuggestion = suggestionResults.first();
+ if (!AutoCorrectionUtils.suggestionExceedsAutoCorrectionThreshold(
+ firstSuggestion, consideredWord, mAutoCorrectionThreshold)) {
+ // Score is too low for autocorrect
+ hasAutoCorrection = false;
+ } else {
+ // We have a high score, so we need to check if this suggestion is in the correct
+ // form to allow auto-correcting to it in this language. For details of how this
+ // is determined, see #isAllowedByAutoCorrectionWithSpaceFilter.
+ // TODO: this should not have its own logic here but be handled by the dictionary.
+ hasAutoCorrection = isAllowedByAutoCorrectionWithSpaceFilter(firstSuggestion);
+ }
}
if (!TextUtils.isEmpty(typedWord)) {
@@ -207,13 +242,15 @@ public final class Suggest {
// Retrieves suggestions for the batch input
// and calls the callback function with the suggestions.
private void getSuggestedWordsForBatchInput(final WordComposer wordComposer,
- final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
+ final NgramContext ngramContext, final ProximityInfo proximityInfo,
final SettingsValuesForSuggestion settingsValuesForSuggestion,
final int inputStyle, final int sequenceNumber,
final OnGetSuggestedWordsCallback callback) {
final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
- wordComposer, prevWordsInfo, proximityInfo, settingsValuesForSuggestion,
- SESSION_ID_GESTURE);
+ wordComposer, ngramContext, proximityInfo.getNativeProximityInfo(),
+ settingsValuesForSuggestion, SESSION_ID_GESTURE);
+ // For transforming words that don't come from a dictionary, because it's our best bet
+ final Locale defaultLocale = mDictionaryFacilitator.getMostProbableLocale();
final ArrayList<SuggestedWordInfo> suggestionsContainer =
new ArrayList<>(suggestionResults);
final int suggestionsCount = suggestionsContainer.size();
@@ -222,9 +259,10 @@ public final class Suggest {
if (isFirstCharCapitalized || isAllUpperCase) {
for (int i = 0; i < suggestionsCount; ++i) {
final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
+ final Locale wordlocale = wordInfo.mSourceDict.mLocale;
final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
- wordInfo, suggestionResults.mLocale, isAllUpperCase, isFirstCharCapitalized,
- 0 /* trailingSingleQuotesCount */);
+ wordInfo, null == wordlocale ? defaultLocale : wordlocale, isAllUpperCase,
+ isFirstCharCapitalized, 0 /* trailingSingleQuotesCount */);
suggestionsContainer.set(i, transformedWordInfo);
}
}
@@ -237,7 +275,7 @@ public final class Suggest {
SuggestedWordInfo.removeDups(null /* typedWord */, suggestionsContainer);
// For some reason some suggestions with MIN_VALUE are making their way here.
- // TODO: Find a more robust way to detect distractors.
+ // TODO: Find a more robust way to detect distracters.
for (int i = suggestionsContainer.size() - 1; i >= 0; --i) {
if (suggestionsContainer.get(i).mScore < SUPPRESS_SUGGEST_THRESHOLD) {
suggestionsContainer.remove(i);
@@ -283,6 +321,41 @@ public final class Suggest {
return suggestionsList;
}
+ /**
+ * Computes whether this suggestion should be blocked or not in this language
+ *
+ * This function implements a filter that avoids auto-correcting to suggestions that contain
+ * spaces that are above a certain language-dependent character limit. In languages like German
+ * where it's possible to concatenate many words, it often happens our dictionary does not
+ * have the longer words. In this case, we offer a lot of unhelpful suggestions that contain
+ * one or several spaces. Ideally we should understand what the user wants and display useful
+ * suggestions by improving the dictionary and possibly having some specific logic. Until
+ * that's possible we should avoid displaying unhelpful suggestions. But it's hard to tell
+ * whether a suggestion is useful or not. So at least for the time being we block
+ * auto-correction when the suggestion is long and contains a space, which should avoid the
+ * worst damage.
+ * This function is implementing that filter. If the language enforces no such limit, then it
+ * always returns true. If the suggestion contains no space, it also returns true. Otherwise,
+ * it checks the length against the language-specific limit.
+ *
+ * @param info the suggestion info
+ * @return whether it's fine to auto-correct to this.
+ */
+ private static boolean isAllowedByAutoCorrectionWithSpaceFilter(final SuggestedWordInfo info) {
+ final Locale locale = info.mSourceDict.mLocale;
+ if (null == locale) {
+ return true;
+ }
+ final Integer maximumLengthForThisLanguage =
+ sLanguageToMaximumAutoCorrectionWithSpaceLength.get(locale.getLanguage());
+ if (null == maximumLengthForThisLanguage) {
+ // This language does not enforce a maximum length to auto-correction
+ return true;
+ }
+ return info.mWord.length() <= maximumLengthForThisLanguage
+ || -1 == info.mWord.indexOf(Constants.CODE_SPACE);
+ }
+
/* package for test */ static SuggestedWordInfo getTransformedSuggestedWordInfo(
final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase,
final boolean isOnlyFirstCharCapitalized, final int trailingSingleQuotesCount) {
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 1d221b77f..c51e20f21 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -20,13 +20,16 @@ import android.text.TextUtils;
import android.view.inputmethod.CompletionInfo;
import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.define.DebugFlags;
-import com.android.inputmethod.latin.utils.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
+import javax.annotation.Nonnull;
+
public class SuggestedWords {
public static final int INDEX_OF_TYPED_WORD = 0;
public static final int INDEX_OF_AUTO_CORRECTION = 1;
@@ -45,7 +48,8 @@ public class SuggestedWords {
public static final int MAX_SUGGESTIONS = 18;
private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = new ArrayList<>(0);
- public static final SuggestedWords EMPTY = new SuggestedWords(
+ @Nonnull
+ private static final SuggestedWords EMPTY = new SuggestedWords(
EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false /* typedWordValid */,
false /* willAutoCorrect */, false /* isObsoleteSuggestions */, INPUT_STYLE_NONE);
@@ -113,6 +117,19 @@ public class SuggestedWords {
}
/**
+ * Get suggested word to show as suggestions to UI.
+ *
+ * @param shouldShowLxxSuggestionUi true if showing suggestion UI introduced in LXX and later.
+ * @return the count of suggested word to show as suggestions to UI.
+ */
+ public int getWordCountToShow(final boolean shouldShowLxxSuggestionUi) {
+ if (isPrediction() || !shouldShowLxxSuggestionUi) {
+ return size();
+ }
+ return size() - /* typed word */ 1;
+ }
+
+ /**
* Get suggested word at <code>index</code>.
* @param index The index of the suggested word.
* @return The suggested word.
@@ -142,6 +159,15 @@ public class SuggestedWords {
return mSuggestedWordInfoList.get(index);
}
+ /**
+ * Gets the suggestion index from the suggestions list.
+ * @param suggestedWordInfo The {@link SuggestedWordInfo} to find the index.
+ * @return The position of the suggestion in the suggestion list.
+ */
+ public int indexOf(SuggestedWordInfo suggestedWordInfo) {
+ return mSuggestedWordInfoList.indexOf(suggestedWordInfo);
+ }
+
public String getDebugString(final int pos) {
if (!DebugFlags.DEBUG_ENABLED) {
return null;
@@ -187,6 +213,11 @@ public class SuggestedWords {
return result;
}
+ @Nonnull
+ public static final SuggestedWords getEmptyInstance() {
+ return SuggestedWords.EMPTY;
+ }
+
// Should get rid of the first one (what the user typed previously) from suggestions
// and replace it with what the user currently typed.
public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions(
@@ -217,7 +248,8 @@ public class SuggestedWords {
return candidate.isEligibleForAutoCommit() ? candidate : null;
}
- public static final class SuggestedWordInfo {
+ // non-final for testability.
+ public static class SuggestedWordInfo {
public static final int NOT_AN_INDEX = -1;
public static final int NOT_A_CONFIDENCE = -1;
public static final int MAX_SCORE = Integer.MAX_VALUE;
@@ -338,9 +370,8 @@ public class SuggestedWords {
public String toString() {
if (TextUtils.isEmpty(mDebugString)) {
return mWord;
- } else {
- return mWord + " (" + mDebugString + ")";
}
+ return mWord + " (" + mDebugString + ")";
}
// This will always remove the higher index if a duplicate is found.
@@ -387,28 +418,6 @@ public class SuggestedWords {
return isPrediction(mInputStyle);
}
- // SuggestedWords is an immutable object, as much as possible. We must not just remove
- // words from the member ArrayList as some other parties may expect the object to never change.
- // This is only ever called by recorrection at the moment, hence the ForRecorrection moniker.
- public SuggestedWords getSuggestedWordsExcludingTypedWordForRecorrection() {
- final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>();
- String typedWord = null;
- for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) {
- final SuggestedWordInfo info = mSuggestedWordInfoList.get(i);
- if (!info.isKindOf(SuggestedWordInfo.KIND_TYPED)) {
- newSuggestions.add(info);
- } else {
- assert(null == typedWord);
- typedWord = info.mWord;
- }
- }
- // We should never autocorrect, so we say the typed word is valid. Also, in this case,
- // no auto-correction should take place hence willAutoCorrect = false.
- return new SuggestedWords(newSuggestions, null /* rawSuggestions */, typedWord,
- true /* typedWordValid */, false /* willAutoCorrect */, mIsObsoleteSuggestions,
- SuggestedWords.INPUT_STYLE_RECORRECTION, NOT_A_SEQUENCE_NUMBER);
- }
-
// Creates a new SuggestedWordInfo from the currently suggested words that removes all but the
// last word of all suggestions, separated by a space. This is necessary because when we commit
// a multiple-word suggestion, the IME only retains the last word as the composing word, and
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 21014b378..2b7fb1748 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -28,7 +28,7 @@ import android.provider.UserDictionary.Words;
import android.text.TextUtils;
import android.util.Log;
-import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.annotations.ExternallyReferenced;
import com.android.inputmethod.compat.UserDictionaryCompatUtils;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
@@ -64,7 +64,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
private static final String NAME = "userunigram";
private ContentObserver mObserver;
- final private String mLocale;
+ final private String mLocaleString;
final private boolean mAlsoUseMoreRestrictiveLocales;
protected UserBinaryDictionary(final Context context, final Locale locale,
@@ -74,9 +74,9 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
final String localeStr = locale.toString();
if (SubtypeLocaleUtils.NO_LANGUAGE.equals(localeStr)) {
// If we don't have a locale, insert into the "all locales" user dictionary.
- mLocale = USER_DICTIONARY_ALL_LANGUAGES;
+ mLocaleString = USER_DICTIONARY_ALL_LANGUAGES;
} else {
- mLocale = localeStr;
+ mLocaleString = localeStr;
}
mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
ContentResolver cres = context.getContentResolver();
@@ -101,7 +101,8 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
reloadDictionaryIfRequired();
}
- @UsedForTesting
+ // Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
+ @ExternallyReferenced
public static UserBinaryDictionary getDictionary(final Context context, final Locale locale,
final File dictFile, final String dictNamePrefix) {
return new UserBinaryDictionary(context, locale, false /* alsoUseMoreRestrictiveLocales */,
@@ -124,7 +125,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
// 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);
+ TextUtils.isEmpty(mLocaleString) ? new String[] {} : mLocaleString.split("_", 3);
final int length = localeElements.length;
final StringBuilder request = new StringBuilder("(locale is NULL)");
@@ -207,9 +208,8 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
if (client != null) {
client.release();
return true;
- } else {
- return false;
}
+ return false;
}
/**
@@ -227,17 +227,16 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY, null, locale);
}
- private int scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency) {
+ private static int scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency) {
// The default frequency for the user dictionary is 250 for historical reasons.
// Latin IME considers a good value for the default user dictionary frequency
// is about 160 considering the scale we use. So we are scaling down the values.
if (defaultFrequency > Integer.MAX_VALUE / LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY) {
return (defaultFrequency / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY)
* LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY;
- } else {
- return (defaultFrequency * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY)
- / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY;
}
+ return (defaultFrequency * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY)
+ / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY;
}
private void addWordsLocked(final Cursor cursor) {
@@ -257,12 +256,14 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
runGCIfRequiredLocked(true /* mindsBlockByGC */);
addUnigramLocked(word, adjustedFrequency, null /* shortcutTarget */,
0 /* shortcutFreq */, false /* isNotAWord */,
- false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
+ false /* isPossiblyOffensive */,
+ BinaryDictionary.NOT_A_VALID_TIMESTAMP);
if (null != shortcut && shortcut.length() <= MAX_WORD_LENGTH) {
runGCIfRequiredLocked(true /* mindsBlockByGC */);
addUnigramLocked(shortcut, adjustedFrequency, word,
USER_DICT_SHORTCUT_FREQUENCY, true /* isNotAWord */,
- false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
+ false /* isPossiblyOffensive */,
+ BinaryDictionary.NOT_A_VALID_TIMESTAMP);
}
}
cursor.moveToNext();
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 32d1fe372..fa55319d2 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -18,9 +18,13 @@ package com.android.inputmethod.latin;
import com.android.inputmethod.event.CombinerChain;
import com.android.inputmethod.event.Event;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.InputPointers;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.define.DebugFlags;
import com.android.inputmethod.latin.utils.CoordinateUtils;
-import com.android.inputmethod.latin.utils.StringUtils;
import java.util.ArrayList;
import java.util.Collections;
@@ -48,7 +52,7 @@ public final class WordComposer {
// The list of events that served to compose this string.
private final ArrayList<Event> mEvents;
private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH);
- private String mAutoCorrection;
+ private SuggestedWordInfo mAutoCorrection;
private boolean mIsResumed;
private boolean mIsBatchMode;
// A memory of the last rejected batch mode suggestion, if any. This goes like this: the user
@@ -87,6 +91,10 @@ public final class WordComposer {
refreshTypedWordCache();
}
+ public ComposedData getComposedDataSnapshot() {
+ return new ComposedData(getInputPointers(), isBatchMode(), mTypedWordCache.toString());
+ }
+
/**
* Restart the combiners, possibly with a new spec.
* @param combiningSpec The spec string for combining. This is found in the extra value.
@@ -127,43 +135,10 @@ public final class WordComposer {
* Number of keystrokes in the composing word.
* @return the number of keystrokes
*/
- // This may be made public if need be, but right now it's not used anywhere
- /* package for tests */ int size() {
+ public int size() {
return mCodePointSize;
}
- /**
- * Copy the code points in the typed word to a destination array of ints.
- *
- * If the array is too small to hold the code points in the typed word, nothing is copied and
- * -1 is returned.
- *
- * @param destination the array of ints.
- * @return the number of copied code points.
- */
- public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
- final int[] destination) {
- // This method can be called on a separate thread and mTypedWordCache can change while we
- // are executing this method.
- final String typedWord = mTypedWordCache.toString();
- // lastIndex is exclusive
- final int lastIndex = typedWord.length()
- - StringUtils.getTrailingSingleQuotesCount(typedWord);
- if (lastIndex <= 0) {
- // The string is empty or contains only single quotes.
- return 0;
- }
-
- // The following function counts the number of code points in the text range which begins
- // at index 0 and extends to the character at lastIndex.
- final int codePointSize = Character.codePointCount(typedWord, 0, lastIndex);
- if (codePointSize > destination.length) {
- return -1;
- }
- return StringUtils.copyCodePointsAndReturnCodePointCount(destination, typedWord, 0,
- lastIndex, true /* downCase */);
- }
-
public boolean isSingleLetter() {
return size() == 1;
}
@@ -182,7 +157,7 @@ public final class WordComposer {
* @return the processed event. Never null, but may be marked as consumed.
*/
@Nonnull
- public Event processEvent(final Event event) {
+ public Event processEvent(@Nonnull final Event event) {
final Event processedEvent = mCombinerChain.processEvent(mEvents, event);
// The retained state of the combiner chain may have changed while processing the event,
// so we need to update our cache.
@@ -353,9 +328,8 @@ public final class WordComposer {
if (size() <= 1) {
return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
|| mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED;
- } else {
- return mCapsCount == size();
}
+ return mCapsCount == size();
}
public boolean wasShiftedNoLock() {
@@ -418,14 +392,14 @@ public final class WordComposer {
/**
* Sets the auto-correction for this word.
*/
- public void setAutoCorrection(final String correction) {
- mAutoCorrection = correction;
+ public void setAutoCorrection(final SuggestedWordInfo autoCorrection) {
+ mAutoCorrection = autoCorrection;
}
/**
* @return the auto-correction for this word, or null if none.
*/
- public String getAutoCorrectionOrNull() {
+ public SuggestedWordInfo getAutoCorrectionOrNull() {
return mAutoCorrection;
}
@@ -439,13 +413,13 @@ public final class WordComposer {
// `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
// committedWord should contain suggestion spans if applicable.
public LastComposedWord commitWord(final int type, final CharSequence committedWord,
- final String separatorString, final PrevWordsInfo prevWordsInfo) {
+ final String separatorString, final NgramContext ngramContext) {
// Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
// 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 LastComposedWord lastComposedWord = new LastComposedWord(mEvents,
mInputPointers, mTypedWordCache.toString(), committedWord, separatorString,
- prevWordsInfo, mCapitalizedMode);
+ ngramContext, mCapitalizedMode);
mInputPointers.reset();
if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
&& type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
diff --git a/java/src/com/android/inputmethod/latin/accounts/AccountsChangedReceiver.java b/java/src/com/android/inputmethod/latin/accounts/AccountsChangedReceiver.java
new file mode 100644
index 000000000..00bcecf52
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/accounts/AccountsChangedReceiver.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2014 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.accounts;
+
+import android.accounts.AccountManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.settings.LocalSettingsConstants;
+
+/**
+ * {@link BroadcastReceiver} for {@link AccountManager#LOGIN_ACCOUNTS_CHANGED_ACTION}.
+ */
+public class AccountsChangedReceiver extends BroadcastReceiver {
+ static final String TAG = "AccountsChangedReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(intent.getAction())) {
+ Log.w(TAG, "Received unknown broadcast: " + intent);
+ return;
+ }
+
+ // Ideally the account preference could live in a different preferences file
+ // that wasn't being backed up and restored, however the preference fragments
+ // currently only deal with the default shared preferences which is why
+ // separating this out into a different file is not trivial currently.
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ final String currentAccount = prefs.getString(
+ LocalSettingsConstants.PREF_ACCOUNT_NAME, null);
+ removeUnknownAccountFromPreference(prefs, getAccountsForLogin(context), currentAccount);
+ }
+
+ /**
+ * Helper method to help test this receiver.
+ */
+ @UsedForTesting
+ protected String[] getAccountsForLogin(Context context) {
+ return LoginAccountUtils.getAccountsForLogin(context);
+ }
+
+ /**
+ * Removes the currentAccount from preferences if it's not found
+ * in the list of current accounts.
+ */
+ private static void removeUnknownAccountFromPreference(final SharedPreferences prefs,
+ final String[] accounts, final String currentAccount) {
+ if (currentAccount == null) {
+ return;
+ }
+ for (final String account : accounts) {
+ if (TextUtils.equals(currentAccount, account)) {
+ return;
+ }
+ }
+ Log.i(TAG, "The current account was removed from the system: " + currentAccount);
+ prefs.edit()
+ .remove(LocalSettingsConstants.PREF_ACCOUNT_NAME)
+ .apply();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/accounts/AuthUtils.java b/java/src/com/android/inputmethod/latin/accounts/AuthUtils.java
new file mode 100644
index 000000000..31aba3631
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/accounts/AuthUtils.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2014 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.accounts;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+
+import java.io.IOException;
+
+/**
+ * Utility class that handles generation/invalidation of auth tokens in the app.
+ */
+public class AuthUtils {
+ private final AccountManager mAccountManager;
+
+ public AuthUtils(Context context) {
+ mAccountManager = AccountManager.get(context);
+ }
+
+ /**
+ * @see AccountManager#invalidateAuthToken(String, String)
+ */
+ public void invalidateAuthToken(final String accountType, final String authToken) {
+ mAccountManager.invalidateAuthToken(accountType, authToken);
+ }
+
+ /**
+ * @see AccountManager#getAuthToken(
+ * Account, String, Bundle, boolean, AccountManagerCallback, Handler)
+ */
+ public AccountManagerFuture<Bundle> getAuthToken(final Account account,
+ final String authTokenType, final Bundle options, final boolean notifyAuthFailure,
+ final AccountManagerCallback<Bundle> callback, final Handler handler) {
+ return mAccountManager.getAuthToken(account, authTokenType, options, notifyAuthFailure,
+ callback, handler);
+ }
+
+ /**
+ * @see AccountManager#blockingGetAuthToken(Account, String, boolean)
+ */
+ public String blockingGetAuthToken(final Account account, final String authTokenType,
+ final boolean notifyAuthFailure) throws OperationCanceledException,
+ AuthenticatorException, IOException {
+ return mAccountManager.blockingGetAuthToken(account, authTokenType, notifyAuthFailure);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java
index a87785b1a..d4be0e397 100644
--- a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java
+++ b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java
@@ -44,7 +44,7 @@ import java.util.Locale;
* A class to read a local file as a dictionary for debugging purposes.
*/
public class ExternalDictionaryGetterForDebug {
- private static final String SOURCE_FOLDER = Environment.getExternalStorageDirectory().getPath()
+ static final String SOURCE_FOLDER = Environment.getExternalStorageDirectory().getPath()
+ "/Download";
private static String[] findDictionariesInTheDownloadedFolder() {
@@ -142,8 +142,7 @@ public class ExternalDictionaryGetterForDebug {
}).create().show();
}
- private static void installFile(final Context context, final File file,
- final DictionaryHeader header) {
+ static void installFile(final Context context, final File file, final DictionaryHeader header) {
BufferedOutputStream outputStream = null;
File tempFile = null;
try {
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index fdab7f25f..bafea178e 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -39,21 +39,21 @@ import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.keyboard.TextDecorator;
import com.android.inputmethod.keyboard.TextDecoratorUiOperator;
-import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.DictionaryFacilitator;
-import com.android.inputmethod.latin.InputPointers;
import com.android.inputmethod.latin.LastComposedWord;
import com.android.inputmethod.latin.LatinIME;
-import com.android.inputmethod.latin.PrevWordsInfo;
+import com.android.inputmethod.latin.NgramContext;
import com.android.inputmethod.latin.RichInputConnection;
import com.android.inputmethod.latin.Suggest;
import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.InputPointers;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.define.DebugFlags;
-import com.android.inputmethod.latin.define.ProductionFlags;
import com.android.inputmethod.latin.settings.SettingsValues;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
@@ -61,13 +61,15 @@ import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
import com.android.inputmethod.latin.utils.AsyncResultHolder;
import com.android.inputmethod.latin.utils.InputTypeUtils;
import com.android.inputmethod.latin.utils.RecapitalizeStatus;
-import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.utils.StatsUtils;
import com.android.inputmethod.latin.utils.TextRange;
import java.util.ArrayList;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
+import javax.annotation.Nonnull;
+
/**
* This class manages the input logic.
*/
@@ -75,7 +77,7 @@ public final class InputLogic {
private static final String TAG = InputLogic.class.getSimpleName();
// TODO : Remove this member when we can.
- private final LatinIME mLatinIME;
+ final LatinIME mLatinIME;
private final SuggestionStripViewAccessor mSuggestionStripViewAccessor;
// Never null.
@@ -85,7 +87,7 @@ public final class InputLogic {
// Current space state of the input method. This can be any of the above constants.
private int mSpaceState;
// Never null
- public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
+ public SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance();
public final Suggest mSuggest;
private final DictionaryFacilitator mDictionaryFacilitator;
@@ -144,13 +146,20 @@ public final class InputLogic {
*/
public void startInput(final String combiningSpec, final SettingsValues settingsValues) {
mEnteredText = null;
+ if (!mWordComposer.getTypedWord().isEmpty()) {
+ // For messaging apps that offer send button, the IME does not get the opportunity
+ // to capture the last word. This block should capture those uncommitted words.
+ // The timestamp at which it is captured is not accurate but close enough.
+ StatsUtils.onWordCommitUserTyped(
+ mWordComposer.getTypedWord(), mWordComposer.isBatchMode());
+ }
mWordComposer.restartCombining(combiningSpec);
resetComposingState(true /* alsoResetLastComposedWord */);
mDeleteCount = 0;
mSpaceState = SpaceState.NONE;
mRecapitalizeStatus.disable(); // Do not perform recapitalize until the cursor is moved once
mCurrentlyPressedHardwareKeys.clear();
- mSuggestedWords = SuggestedWords.EMPTY;
+ mSuggestedWords = SuggestedWords.getEmptyInstance();
// In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying
// so we try using some heuristics to find out about these and fix them.
mConnection.tryFixLyingCursorPosition();
@@ -161,14 +170,11 @@ public final class InputLogic {
mInputLogicHandler.reset();
}
- if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) {
- // AcceptTypedWord feature relies on CursorAnchorInfo.
- if (settingsValues.mShouldShowUiToAcceptTypedWord) {
- mConnection.requestCursorUpdates(true /* enableMonitor */,
- true /* requestImmediateCallback */);
- }
- mTextDecorator.reset();
+ if (settingsValues.mShouldShowLxxSuggestionUi) {
+ mConnection.requestCursorUpdates(true /* enableMonitor */,
+ true /* requestImmediateCallback */);
}
+ mTextDecorator.reset();
}
/**
@@ -204,6 +210,8 @@ public final class InputLogic {
public void finishInput() {
if (mWordComposer.isComposingWord()) {
mConnection.finishComposingText();
+ StatsUtils.onWordCommitUserTyped(
+ mWordComposer.getTypedWord(), mWordComposer.isBatchMode());
}
resetComposingState(true /* alsoResetLastComposedWord */);
mInputLogicHandler.reset();
@@ -250,6 +258,7 @@ public final class InputLogic {
promotePhantomSpace(settingsValues);
}
mConnection.commitText(text, 1);
+ StatsUtils.onWordCommitUserTyped(mEnteredText, mWordComposer.isBatchMode());
mConnection.endBatchEdit();
// Space state must be updated before calling updateShiftState
mSpaceState = SpaceState.NONE;
@@ -298,6 +307,7 @@ public final class InputLogic {
currentKeyboardScriptId, handler);
}
+ mDictionaryFacilitator.switchMostProbableLanguage(suggestionInfo.mSourceDict.mLocale);
final Event event = Event.createSuggestionPickedEvent(suggestionInfo);
final InputTransaction inputTransaction = new InputTransaction(settingsValues,
event, SystemClock.uptimeMillis(), mSpaceState, keyboardShiftState);
@@ -321,7 +331,7 @@ public final class InputLogic {
// however need to reset the suggestion strip right away, because we know we can't take
// the risk of calling commitCompletion twice because we don't know how the app will react.
if (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_APP_DEFINED)) {
- mSuggestedWords = SuggestedWords.EMPTY;
+ mSuggestedWords = SuggestedWords.getEmptyInstance();
mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
resetComposingState(true /* alsoResetLastComposedWord */);
@@ -341,12 +351,17 @@ public final class InputLogic {
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
if (shouldShowAddToDictionaryHint) {
- mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion);
+ mSuggestionStripViewAccessor.suggestAddingToDictionary(suggestion,
+ true /* isFromSuggestionStrip */);
} else {
// If we're not showing the "Touch again to save", then update the suggestion strip.
// That's going to be predictions (or punctuation suggestions), so INPUT_STYLE_NONE.
handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE);
}
+
+ StatsUtils.onPickSuggestionManually(mSuggestedWords, suggestionInfo);
+ StatsUtils.onWordCommitSuggestionPickedManually(
+ suggestionInfo.mWord, mWordComposer.isBatchMode());
return inputTransaction;
}
@@ -422,8 +437,7 @@ public final class InputLogic {
// removed.
mConnection.removeBackgroundColorFromHighlightedTextIfNecessary();
// We moved the cursor. If we are touching a word, we need to resume suggestion.
- mLatinIME.mHandler.postResumeSuggestions(false /* shouldIncludeResumedWordInSuggestions */,
- true /* shouldDelay */);
+ mLatinIME.mHandler.postResumeSuggestions(true /* shouldDelay */);
// Stop the last recapitalization, if started.
mRecapitalizeStatus.stop();
return true;
@@ -442,8 +456,8 @@ public final class InputLogic {
* {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()}
* @return the complete transaction object
*/
- public InputTransaction onCodeInput(final SettingsValues settingsValues, final Event event,
- final int keyboardShiftMode,
+ public InputTransaction onCodeInput(final SettingsValues settingsValues,
+ @Nonnull final Event event, final int keyboardShiftMode,
// TODO: remove these arguments
final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
final Event processedEvent = mWordComposer.processEvent(event);
@@ -495,7 +509,7 @@ public final class InputLogic {
final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
mInputLogicHandler.onStartBatchInput();
handler.showGesturePreviewAndSuggestionStrip(
- SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
+ SuggestedWords.getEmptyInstance(), false /* dismissGestureFloatingPreviewText */);
handler.cancelUpdateSuggestionStrip();
++mAutoCommitSequenceNumber;
mConnection.beginBatchEdit();
@@ -572,6 +586,7 @@ public final class InputLogic {
batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
promotePhantomSpace(settingsValues);
mConnection.commitText(commitParts[0], 0);
+ StatsUtils.onWordCommitUserTyped(commitParts[0], mWordComposer.isBatchMode());
mSpaceState = SpaceState.PHANTOM;
keyboardSwitcher.requestUpdatingShiftState(
getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState());
@@ -593,23 +608,26 @@ public final class InputLogic {
public void onCancelBatchInput(final LatinIME.UIHandler handler) {
mInputLogicHandler.onCancelBatchInput();
handler.showGesturePreviewAndSuggestionStrip(
- SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
+ SuggestedWords.getEmptyInstance(), true /* dismissGestureFloatingPreviewText */);
}
// TODO: on the long term, this method should become private, but it will be difficult.
// Especially, how do we deal with InputMethodService.onDisplayCompletions?
- public void setSuggestedWords(final SuggestedWords suggestedWords,
- final SettingsValues settingsValues, final LatinIME.UIHandler handler) {
- if (SuggestedWords.EMPTY != suggestedWords) {
- final String autoCorrection;
+ public void setSuggestedWords(final SuggestedWords suggestedWords) {
+ if (!suggestedWords.isEmpty()) {
+ final SuggestedWordInfo suggestedWordInfo;
if (suggestedWords.mWillAutoCorrect) {
- autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
+ suggestedWordInfo = suggestedWords.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
} else {
// We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)
// because it may differ from mWordComposer.mTypedWord.
- autoCorrection = suggestedWords.mTypedWord;
+ suggestedWordInfo = new SuggestedWordInfo(suggestedWords.mTypedWord,
+ SuggestedWordInfo.MAX_SCORE,
+ SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+ SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */);
}
- mWordComposer.setAutoCorrection(autoCorrection);
+ mWordComposer.setAutoCorrection(suggestedWordInfo);
}
mSuggestedWords = suggestedWords;
final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect;
@@ -683,7 +701,7 @@ public final class InputLogic {
break;
case Constants.CODE_CAPSLOCK:
// Note: Changing keyboard to shift lock state is handled in
- // {@link KeyboardSwitcher#onCodeInput(int)}.
+ // {@link KeyboardSwitcher#onEvent(Event)}.
break;
case Constants.CODE_SYMBOL_SHIFT:
// Note: Calling back to the keyboard on the symbol Shift key is handled in
@@ -711,11 +729,11 @@ public final class InputLogic {
break;
case Constants.CODE_EMOJI:
// Note: Switching emoji keyboard is being handled in
- // {@link KeyboardState#onCodeInput(int,int)}.
+ // {@link KeyboardState#onEvent(Event,int)}.
break;
case Constants.CODE_ALPHA_FROM_EMOJI:
// Note: Switching back from Emoji keyboard to the main keyboard is being
- // handled in {@link KeyboardState#onCodeInput(int,int)}.
+ // handled in {@link KeyboardState#onEvent(Event,int)}.
break;
case Constants.CODE_SHIFT_ENTER:
// TODO: remove this object
@@ -1043,8 +1061,10 @@ public final class InputLogic {
if (!TextUtils.isEmpty(rejectedSuggestion)) {
mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion);
}
+ StatsUtils.onBackspaceWordDelete(rejectedSuggestion.length());
} else {
mWordComposer.applyProcessedEvent(event);
+ StatsUtils.onBackspacePressed(1);
}
if (mWordComposer.isComposingWord()) {
setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
@@ -1054,7 +1074,10 @@ public final class InputLogic {
inputTransaction.setRequiresUpdateSuggestions();
} else {
if (mLastComposedWord.canRevertCommit()) {
+ final String lastComposedWord = mLastComposedWord.mTypedWord;
revertCommit(inputTransaction, inputTransaction.mSettingsValues);
+ StatsUtils.onRevertAutoCorrect();
+ StatsUtils.onWordCommitUserTyped(lastComposedWord, mWordComposer.isBatchMode());
return;
}
if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
@@ -1062,6 +1085,7 @@ public final class InputLogic {
// This is triggered on backspace after a key that inputs multiple characters,
// like the smiley key or the .com key.
mConnection.deleteSurroundingText(mEnteredText.length(), 0);
+ StatsUtils.onDeleteMultiCharInput(mEnteredText.length());
mEnteredText = null;
// If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
// In addition we know that spaceState is false, and that we should not be
@@ -1070,16 +1094,19 @@ public final class InputLogic {
}
if (SpaceState.DOUBLE == inputTransaction.mSpaceState) {
cancelDoubleSpacePeriodCountdown();
- if (mConnection.revertDoubleSpacePeriod()) {
+ if (mConnection.revertDoubleSpacePeriod(
+ inputTransaction.mSettingsValues.mSpacingAndPunctuations)) {
// No need to reset mSpaceState, it has already be done (that's why we
// receive it as a parameter)
inputTransaction.setRequiresUpdateSuggestions();
mWordComposer.setCapitalizedModeAtStartComposingTime(
WordComposer.CAPS_MODE_OFF);
+ StatsUtils.onRevertDoubleSpacePeriod();
return;
}
} else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) {
if (mConnection.revertSwapPunctuation()) {
+ StatsUtils.onRevertSwapPunctuation();
// Likewise
return;
}
@@ -1094,25 +1121,31 @@ public final class InputLogic {
mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
mConnection.getExpectedSelectionEnd());
mConnection.deleteSurroundingText(numCharsDeleted, 0);
+ StatsUtils.onBackspaceSelectedText(numCharsDeleted);
} else {
// There is no selection, just delete one character.
- if (Constants.NOT_A_CURSOR_POSITION == mConnection.getExpectedSelectionEnd()) {
- // This should never happen.
- Log.e(TAG, "Backspace when we don't know the selection position");
- }
- if (inputTransaction.mSettingsValues.isBeforeJellyBean() ||
- inputTransaction.mSettingsValues.mInputAttributes.isTypeNull()) {
- // There are two possible reasons to send a key event: either the field has
+ if (inputTransaction.mSettingsValues.isBeforeJellyBean()
+ || inputTransaction.mSettingsValues.mInputAttributes.isTypeNull()
+ || Constants.NOT_A_CURSOR_POSITION
+ == mConnection.getExpectedSelectionEnd()) {
+ // There are three possible reasons to send a key event: either the field has
// type TYPE_NULL, in which case the keyboard should send events, or we are
- // running in 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, so we retain this behavior if the app has target SDK < JellyBean.
+ // running in backward compatibility mode, or we don't know the cursor position.
+ // 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, so we retain this behavior if
+ // the app has target SDK < JellyBean.
+ // As for the case where we don't know the cursor position, it can happen
+ // because of bugs in the framework. But the framework should know, so the next
+ // best thing is to leave it to whatever it thinks is best.
sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
+ int totalDeletedLength = 1;
if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
+ totalDeletedLength++;
}
+ StatsUtils.onBackspacePressed(totalDeletedLength);
} else {
final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
if (codePointBeforeCursor == Constants.NOT_A_CODE) {
@@ -1123,11 +1156,13 @@ public final class InputLogic {
// catch it and have their broken interface react. If you need the keyboard
// to do this, you're doing it wrong -- please fix your app.
mConnection.deleteSurroundingText(1, 0);
+ // TODO: Add a new StatsUtils method onBackspaceWhenNoText()
return;
}
final int lengthToDelete =
Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
mConnection.deleteSurroundingText(lengthToDelete, 0);
+ int totalDeletedLength = lengthToDelete;
if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
final int codePointBeforeCursorToDeleteAgain =
mConnection.getCodePointBeforeCursor();
@@ -1135,8 +1170,10 @@ public final class InputLogic {
final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
codePointBeforeCursorToDeleteAgain) ? 2 : 1;
mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
+ totalDeletedLength += lengthToDeleteAgain;
}
}
+ StatsUtils.onBackspacePressed(totalDeletedLength);
}
}
if (inputTransaction.mSettingsValues
@@ -1146,7 +1183,7 @@ public final class InputLogic {
&& !mConnection.isCursorFollowedByWordCharacter(
inputTransaction.mSettingsValues.mSpacingAndPunctuations)) {
restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues,
- true /* shouldIncludeResumedWordInSuggestions */, currentKeyboardScriptId);
+ currentKeyboardScriptId);
}
}
}
@@ -1253,7 +1290,9 @@ public final class InputLogic {
if (null == lastTwo) return false;
final int length = lastTwo.length();
if (length < 2) return false;
- if (lastTwo.charAt(length - 1) != Constants.CODE_SPACE) return false;
+ if (lastTwo.charAt(length - 1) != Constants.CODE_SPACE) {
+ return false;
+ }
// We know there is a space in pos -1, and we have at least two chars. If we have only two
// chars, isSurrogatePairs can't return true as charAt(1) is a space, so this is fine.
final int firstCodePoint =
@@ -1336,7 +1375,7 @@ public final class InputLogic {
}
private void performAdditionToUserHistoryDictionary(final SettingsValues settingsValues,
- final String suggestion, final PrevWordsInfo prevWordsInfo) {
+ final String suggestion, @Nonnull final NgramContext ngramContext) {
// 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.
@@ -1348,7 +1387,7 @@ public final class InputLogic {
final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds(
System.currentTimeMillis());
mDictionaryFacilitator.addToUserHistory(suggestion, wasAutoCapitalized,
- prevWordsInfo, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive);
+ ngramContext, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive);
}
public void performUpdateSuggestionStripSync(final SettingsValues settingsValues,
@@ -1360,7 +1399,7 @@ public final class InputLogic {
+ "requested!");
}
// Clear the suggestions strip.
- mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.EMPTY);
+ mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.getEmptyInstance());
return;
}
@@ -1400,12 +1439,10 @@ public final class InputLogic {
* do nothing.
*
* @param settingsValues the current values of the settings.
- * @param shouldIncludeResumedWordInSuggestions whether to include the word on which we resume
* suggestions in the suggestion list.
*/
// TODO: make this private.
public void restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues,
- final boolean shouldIncludeResumedWordInSuggestions,
// TODO: remove this argument, put it into settingsValues
final int currentKeyboardScriptId) {
// HACK: We may want to special-case some apps that exhibit bad behavior in case of
@@ -1453,13 +1490,11 @@ public final class InputLogic {
if (numberOfCharsInWordBeforeCursor > expectedCursorPosition) return;
final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>();
final String typedWord = range.mWord.toString();
- if (shouldIncludeResumedWordInSuggestions) {
- suggestions.add(new SuggestedWordInfo(typedWord,
- SuggestedWords.MAX_SUGGESTIONS + 1,
- SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED,
- SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
- SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
- }
+ suggestions.add(new SuggestedWordInfo(typedWord,
+ SuggestedWords.MAX_SUGGESTIONS + 1,
+ SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+ SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
if (!isResumableWord(settingsValues, typedWord)) {
mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
return;
@@ -1479,12 +1514,6 @@ public final class InputLogic {
}
}
final int[] codePoints = StringUtils.toCodePointArray(typedWord);
- // We want the previous word for suggestion. If we have chars in the word
- // before the cursor, then we want the word before that, hence 2; otherwise,
- // we want the word immediately before the cursor, hence 1.
- final PrevWordsInfo prevWordsInfo = getPrevWordsInfoFromNthPreviousWordForSuggestion(
- settingsValues.mSpacingAndPunctuations,
- 0 == numberOfCharsInWordBeforeCursor ? 1 : 2);
mWordComposer.setComposingWord(codePoints,
mLatinIME.getCoordinatesForCurrentKeyboard(codePoints));
mWordComposer.setCursorPositionWithinWord(
@@ -1492,33 +1521,15 @@ public final class InputLogic {
mConnection.maybeMoveTheCursorAroundAndRestoreToWorkaroundABug();
mConnection.setComposingRegion(expectedCursorPosition - numberOfCharsInWordBeforeCursor,
expectedCursorPosition + range.getNumberOfCharsInWordAfterCursor());
- if (suggestions.size() <= (shouldIncludeResumedWordInSuggestions ? 1 : 0)) {
+ if (suggestions.size() <= 1) {
// If there weren't any suggestion spans on this word, suggestions#size() will be 1
// if shouldIncludeResumedWordInSuggestions is true, 0 otherwise. In this case, we
// have no useful suggestions, so we will try to compute some for it instead.
mInputLogicHandler.getSuggestedWords(Suggest.SESSION_ID_TYPING,
SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
@Override
- public void onGetSuggestedWords(
- final SuggestedWords suggestedWordsIncludingTypedWord) {
- final SuggestedWords suggestedWords;
- if (suggestedWordsIncludingTypedWord.size() > 1
- && !shouldIncludeResumedWordInSuggestions) {
- // We were able to compute new suggestions for this word.
- // Remove the typed word, since we don't want to display it in this
- // case. The #getSuggestedWordsExcludingTypedWordForRecorrection()
- // method sets willAutoCorrect to false.
- suggestedWords = suggestedWordsIncludingTypedWord
- .getSuggestedWordsExcludingTypedWordForRecorrection();
- } else {
- // No saved suggestions, and we were unable to compute any good one
- // either. Rather than displaying an empty suggestion strip, we'll
- // display the original word alone in the middle.
- // Since there is only one word, willAutoCorrect is false.
- suggestedWords = suggestedWordsIncludingTypedWord;
- }
- mIsAutoCorrectionIndicatorOn = false;
- mLatinIME.mHandler.showSuggestionStrip(suggestedWords);
+ public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
+ doShowSuggestionsAndClearAutoCorrectionIndicator(suggestedWords);
}});
} else {
// We found suggestion spans in the word. We'll create the SuggestedWords out of
@@ -1529,11 +1540,15 @@ public final class InputLogic {
null /* rawSuggestions */, typedWord, false /* typedWordValid */,
false /* willAutoCorrect */, false /* isObsoleteSuggestions */,
SuggestedWords.INPUT_STYLE_RECORRECTION, SuggestedWords.NOT_A_SEQUENCE_NUMBER);
- mIsAutoCorrectionIndicatorOn = false;
- mLatinIME.mHandler.showSuggestionStrip(suggestedWords);
+ doShowSuggestionsAndClearAutoCorrectionIndicator(suggestedWords);
}
}
+ void doShowSuggestionsAndClearAutoCorrectionIndicator(final SuggestedWords suggestedWords) {
+ mIsAutoCorrectionIndicatorOn = false;
+ mLatinIME.mHandler.showSuggestionStrip(suggestedWords);
+ }
+
/**
* Reverts a previous commit with auto-correction.
*
@@ -1551,6 +1566,10 @@ public final class InputLogic {
final String committedWordString = committedWord.toString();
final int cancelLength = committedWord.length();
final String separatorString = mLastComposedWord.mSeparatorString;
+ // If our separator is a space, we won't actually commit it,
+ // but set the space state to PHANTOM so that a space will be inserted
+ // on the next keypress
+ final boolean usePhantomSpace = separatorString.equals(Constants.STRING_SPACE);
// We want java chars, not codepoints for the following.
final int separatorLength = separatorString.length();
// TODO: should we check our saved separator against the actual contents of the text view?
@@ -1571,7 +1590,8 @@ public final class InputLogic {
if (!TextUtils.isEmpty(committedWord)) {
mDictionaryFacilitator.removeWordFromPersonalizedDicts(committedWordString);
}
- final String stringToCommit = originallyTypedWord + separatorString;
+ final String stringToCommit = originallyTypedWord +
+ (usePhantomSpace ? "" : separatorString);
final SpannableString textToCommit = new SpannableString(stringToCommit);
if (committedWord instanceof SpannableString) {
final SpannableString committedWordWithSuggestionSpans = (SpannableString)committedWord;
@@ -1604,8 +1624,10 @@ public final class InputLogic {
}
}
// Add the suggestion list to the list of suggestions.
- textToCommit.setSpan(new SuggestionSpan(inputTransaction.mSettingsValues.mLocale,
- suggestions.toArray(new String[suggestions.size()]), 0 /* flags */),
+ textToCommit.setSpan(new SuggestionSpan(mLatinIME /* context */,
+ inputTransaction.mSettingsValues.mLocale,
+ suggestions.toArray(new String[suggestions.size()]), 0 /* flags */,
+ null /* notificationTargetClass */),
0 /* start */, lastCharIndex /* end */, 0 /* flags */);
}
@@ -1623,6 +1645,9 @@ public final class InputLogic {
} else {
mConnection.commitText(textToCommit, 1);
}
+ if (usePhantomSpace) {
+ mSpaceState = SpaceState.PHANTOM;
+ }
} else {
// For languages without spaces, we revert the typed string but the cursor is flush
// with the typed word, so we need to resume suggestions right away.
@@ -1650,7 +1675,8 @@ public final class InputLogic {
mConnection.getExpectedSelectionStart(),
mConnection.getExpectedSelectionEnd());
}
- mSuggestionStripViewAccessor.showAddToDictionaryHint(originallyTypedWordString);
+ mSuggestionStripViewAccessor.suggestAddingToDictionary(originallyTypedWordString,
+ false /* isFromSuggestionStrip */);
} else {
// We have a separator between the word and the cursor: we should show predictions.
inputTransaction.setRequiresUpdateSuggestions();
@@ -1720,26 +1746,26 @@ public final class InputLogic {
}
/**
- * Get information fo previous words from the nth previous word before the cursor as context
+ * Get n-gram context from the nth previous word before the cursor as context
* for the suggestion process.
* @param spacingAndPunctuations the current spacing and punctuations settings.
* @param nthPreviousWord reverse index of the word to get (1-indexed)
* @return the information of previous words
*/
// TODO: Make this private
- public PrevWordsInfo getPrevWordsInfoFromNthPreviousWordForSuggestion(
+ public NgramContext getNgramContextFromNthPreviousWordForSuggestion(
final SpacingAndPunctuations spacingAndPunctuations, final int nthPreviousWord) {
if (spacingAndPunctuations.mCurrentLanguageHasSpaces) {
// If we are typing in a language with spaces we can just look up the previous
// word information from textview.
- return mConnection.getPrevWordsInfoFromNthPreviousWord(
+ return mConnection.getNgramContextFromNthPreviousWord(
spacingAndPunctuations, nthPreviousWord);
- } else {
- return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ?
- PrevWordsInfo.BEGINNING_OF_SENTENCE :
- new PrevWordsInfo(new PrevWordsInfo.WordInfo(
- mLastComposedWord.mCommittedWord.toString()));
}
+ if (LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord) {
+ return NgramContext.BEGINNING_OF_SENTENCE;
+ }
+ return new NgramContext(new NgramContext.WordInfo(
+ mLastComposedWord.mCommittedWord.toString()));
}
/**
@@ -1792,9 +1818,8 @@ public final class InputLogic {
// If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT.
if (Constants.CODE_PERIOD == codePointBeforeCursor) {
return text.substring(1);
- } else {
- return text;
}
+ return text;
}
/**
@@ -1850,11 +1875,10 @@ public final class InputLogic {
* @param previousSuggestedWords The previously suggested words.
* @return Obsolete suggestions with the newly typed word.
*/
- private SuggestedWords retrieveOlderSuggestions(final String typedWord,
+ static SuggestedWords retrieveOlderSuggestions(final String typedWord,
final SuggestedWords previousSuggestedWords) {
- final SuggestedWords oldSuggestedWords =
- previousSuggestedWords.isPunctuationSuggestions() ? SuggestedWords.EMPTY
- : previousSuggestedWords;
+ final SuggestedWords oldSuggestedWords = previousSuggestedWords.isPunctuationSuggestions()
+ ? SuggestedWords.getEmptyInstance() : previousSuggestedWords;
final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords);
return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */,
@@ -1976,6 +2000,8 @@ public final class InputLogic {
final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
if (0 != indexOfLastSpace) {
mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
+ StatsUtils.onWordCommitUserTyped(
+ batchInputText.substring(0, indexOfLastSpace), mWordComposer.isBatchMode());
final SuggestedWords suggestedWordsForLastWordOfPhraseGesture =
suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture();
mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture);
@@ -2014,8 +2040,10 @@ public final class InputLogic {
if (!mWordComposer.isComposingWord()) return;
final String typedWord = mWordComposer.getTypedWord();
if (typedWord.length() > 0) {
+ final boolean isBatchMode = mWordComposer.isBatchMode();
commitChosenWord(settingsValues, typedWord,
LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString);
+ StatsUtils.onWordCommitUserTyped(typedWord, isBatchMode);
}
}
@@ -2052,18 +2080,23 @@ public final class InputLogic {
// INPUT_STYLE_TYPING.
performUpdateSuggestionStripSync(settingsValues, SuggestedWords.INPUT_STYLE_TYPING);
}
- final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
+ final SuggestedWordInfo autoCorrectionOrNull = mWordComposer.getAutoCorrectionOrNull();
final String typedWord = mWordComposer.getTypedWord();
- final String autoCorrection = (typedAutoCorrection != null)
- ? typedAutoCorrection : typedWord;
- if (autoCorrection != null) {
+ final String stringToCommit = (autoCorrectionOrNull != null)
+ ? autoCorrectionOrNull.mWord : typedWord;
+ if (stringToCommit != null) {
if (TextUtils.isEmpty(typedWord)) {
throw new RuntimeException("We have an auto-correction but the typed word "
+ "is empty? Impossible! I must commit suicide.");
}
- commitChosenWord(settingsValues, autoCorrection,
+ final boolean isBatchMode = mWordComposer.isBatchMode();
+ commitChosenWord(settingsValues, stringToCommit,
LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator);
- if (!typedWord.equals(autoCorrection)) {
+ if (null != autoCorrectionOrNull) {
+ mDictionaryFacilitator.switchMostProbableLanguage(
+ autoCorrectionOrNull.mSourceDict.mLocale);
+ }
+ if (!typedWord.equals(stringToCommit)) {
// This will make the correction flash for a short while as a visual clue
// to the user that auto-correction happened. It has no other effect; in particular
// note that this won't affect the text inside the text field AT ALL: it only makes
@@ -2071,8 +2104,14 @@ public final class InputLogic {
// of the auto-correction flash. At this moment, the "typedWord" argument is
// ignored by TextView.
mConnection.commitCorrection(new CorrectionInfo(
- mConnection.getExpectedSelectionEnd() - autoCorrection.length(),
- typedWord, autoCorrection));
+ mConnection.getExpectedSelectionEnd() - stringToCommit.length(),
+ typedWord, stringToCommit));
+ StatsUtils.onAutoCorrection(typedWord, stringToCommit, isBatchMode,
+ null == autoCorrectionOrNull
+ ? null : autoCorrectionOrNull.mSourceDict.mDictType);
+ StatsUtils.onWordCommitAutoCorrect(stringToCommit, isBatchMode);
+ } else {
+ StatsUtils.onWordCommitUserTyped(stringToCommit, isBatchMode);
}
}
}
@@ -2091,20 +2130,20 @@ public final class InputLogic {
final CharSequence chosenWordWithSuggestions =
SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord,
suggestedWords);
- // When we are composing word, get previous words information from the 2nd previous word
- // because the 1st previous word is the word to be committed. Otherwise get previous words
- // information from the 1st previous word.
- final PrevWordsInfo prevWordsInfo = mConnection.getPrevWordsInfoFromNthPreviousWord(
+ // When we are composing word, get n-gram context from the 2nd previous word because the
+ // 1st previous word is the word to be committed. Otherwise get n-gram context from the 1st
+ // previous word.
+ final NgramContext ngramContext = mConnection.getNgramContextFromNthPreviousWord(
settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1);
mConnection.commitText(chosenWordWithSuggestions, 1);
// Add the word to the user history dictionary
- performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWordsInfo);
+ performAdditionToUserHistoryDictionary(settingsValues, chosenWord, ngramContext);
// TODO: figure out here if this is an auto-correct or if the best word is actually
// what user typed. Note: currently this is done much later in
// LastComposedWord#didCommitTypedWord by string equality of the remembered
// strings.
mLastComposedWord = mWordComposer.commitWord(commitType,
- chosenWordWithSuggestions, separatorString, prevWordsInfo);
+ chosenWordWithSuggestions, separatorString, ngramContext);
}
/**
@@ -2137,10 +2176,7 @@ public final class InputLogic {
}
mConnection.tryFixLyingCursorPosition();
if (tryResumeSuggestions) {
- // This is triggered when starting input anew, so we want to include the resumed
- // word in suggestions.
- handler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */,
- true /* shouldDelay */);
+ handler.postResumeSuggestions(true /* shouldDelay */);
}
return true;
}
@@ -2151,7 +2187,7 @@ public final class InputLogic {
mWordComposer.adviseCapitalizedModeBeforeFetchingSuggestions(
getActualCapsMode(settingsValues, keyboardShiftMode));
mSuggest.getSuggestedWords(mWordComposer,
- getPrevWordsInfoFromNthPreviousWordForSuggestion(
+ getNgramContextFromNthPreviousWordForSuggestion(
settingsValues.mSpacingAndPunctuations,
// 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,
@@ -2216,6 +2252,47 @@ public final class InputLogic {
mConnection.setComposingText(composingTextToBeSet, newCursorPosition);
}
+ /**
+ * Gets an object allowing private IME commands to be sent to the
+ * underlying editor.
+ * @return An object for sending private commands to the underlying editor.
+ */
+ public PrivateCommandPerformer getPrivateCommandPerformer() {
+ return mConnection;
+ }
+
+ /**
+ * Gets the expected index of the first char of the composing span within the editor's text.
+ * Returns a negative value in case there appears to be no valid composing span.
+ *
+ * @see #getComposingLength()
+ * @see RichInputConnection#hasSelection()
+ * @see RichInputConnection#isCursorPositionKnown()
+ * @see RichInputConnection#getExpectedSelectionStart()
+ * @see RichInputConnection#getExpectedSelectionEnd()
+ * @return The expected index in Java chars of the first char of the composing span.
+ */
+ // TODO: try and see if we can get rid of this method. Ideally the users of this class should
+ // never need to know this.
+ public int getComposingStart() {
+ if (!mConnection.isCursorPositionKnown() || mConnection.hasSelection()) {
+ return -1;
+ }
+ return mConnection.getExpectedSelectionStart() - mWordComposer.size();
+ }
+
+ /**
+ * Gets the expected length in Java chars of the composing span.
+ * May be 0 if there is no valid composing span.
+ * @see #getComposingStart()
+ * @return The expected length of the composing span.
+ */
+ // TODO: try and see if we can get rid of this method. Ideally the users of this class should
+ // never need to know this.
+ public int getComposingLength() {
+ return mWordComposer.size();
+ }
+
//////////////////////////////////////////////////////////////////////////////////////////////
// Following methods are tentatively placed in this class for the integration with
// TextDecorator.
@@ -2226,7 +2303,7 @@ public final class InputLogic {
* Sets the UI operator for {@link TextDecorator}.
* @param uiOperator the UI operator which should be associated with {@link TextDecorator}.
*/
- public void setTextDecoratorUi(final TextDecoratorUiOperator uiOperator) {
+ public void setTextDecoratorUi(@Nonnull final TextDecoratorUiOperator uiOperator) {
mTextDecorator.setUiOperator(uiOperator);
}
@@ -2267,7 +2344,7 @@ public final class InputLogic {
// We cannot help in this case because we are heavily relying on this new API.
return false;
}
- if (!settingsValues.mShouldShowUiToAcceptTypedWord) {
+ if (!settingsValues.mShouldShowLxxSuggestionUi) {
return false;
}
if (TextUtils.isEmpty(lastComposedWord.mTypedWord)) {
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
index c6f83d0b9..ddc4ad99c 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
@@ -21,11 +21,10 @@ import android.os.HandlerThread;
import android.os.Message;
import com.android.inputmethod.compat.LooperCompatUtils;
-import com.android.inputmethod.latin.InputPointers;
import com.android.inputmethod.latin.LatinIME;
-import com.android.inputmethod.latin.Suggest;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
+import com.android.inputmethod.latin.common.InputPointers;
/**
* A helper to manage deferred tasks for the input logic.
@@ -62,7 +61,7 @@ class InputLogicHandler implements Handler.Callback {
final OnGetSuggestedWordsCallback callback) {}
};
- private InputLogicHandler() {
+ InputLogicHandler() {
mNonUIThreadHandler = null;
mLatinIME = null;
mInputLogic = null;
@@ -134,30 +133,38 @@ class InputLogicHandler implements Handler.Callback {
return;
}
mInputLogic.mWordComposer.setBatchInputPointers(batchPointers);
+ final OnGetSuggestedWordsCallback callback = new OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
+ showGestureSuggestionsWithPreviewVisuals(suggestedWords, isTailBatchInput);
+ }
+ };
getSuggestedWords(isTailBatchInput ? SuggestedWords.INPUT_STYLE_TAIL_BATCH
- : SuggestedWords.INPUT_STYLE_UPDATE_BATCH, sequenceNumber,
- new OnGetSuggestedWordsCallback() {
- @Override
- public void onGetSuggestedWords(SuggestedWords suggestedWords) {
- // We're now inside the callback. This always runs on the Non-UI thread,
- // no matter what thread updateBatchInput was originally called on.
- if (suggestedWords.isEmpty()) {
- // Use old suggestions if we don't have any new ones.
- // Previous suggestions are found in InputLogic#mSuggestedWords.
- // Since these are the most recent ones and we just recomputed
- // new ones to update them, then the previous ones are there.
- suggestedWords = mInputLogic.mSuggestedWords;
- }
- mLatinIME.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords,
- isTailBatchInput /* dismissGestureFloatingPreviewText */);
- if (isTailBatchInput) {
- mInBatchInput = false;
- // The following call schedules onEndBatchInputInternal
- // to be called on the UI thread.
- mLatinIME.mHandler.showTailBatchInputResult(suggestedWords);
- }
- }
- });
+ : SuggestedWords.INPUT_STYLE_UPDATE_BATCH, sequenceNumber, callback);
+ }
+ }
+
+ void showGestureSuggestionsWithPreviewVisuals(final SuggestedWords suggestedWordsForBatchInput,
+ final boolean isTailBatchInput) {
+ final SuggestedWords suggestedWordsToShowSuggestions;
+ // We're now inside the callback. This always runs on the Non-UI thread,
+ // no matter what thread updateBatchInput was originally called on.
+ if (suggestedWordsForBatchInput.isEmpty()) {
+ // Use old suggestions if we don't have any new ones.
+ // Previous suggestions are found in InputLogic#mSuggestedWords.
+ // Since these are the most recent ones and we just recomputed
+ // new ones to update them, then the previous ones are there.
+ suggestedWordsToShowSuggestions = mInputLogic.mSuggestedWords;
+ } else {
+ suggestedWordsToShowSuggestions = suggestedWordsForBatchInput;
+ }
+ mLatinIME.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWordsToShowSuggestions,
+ isTailBatchInput /* dismissGestureFloatingPreviewText */);
+ if (isTailBatchInput) {
+ mInBatchInput = false;
+ // The following call schedules onEndBatchInputInternal
+ // to be called on the UI thread.
+ mLatinIME.mHandler.showTailBatchInputResult(suggestedWordsToShowSuggestions);
}
}
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java b/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java
new file mode 100644
index 000000000..42eaa9c82
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014 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.inputlogic;
+
+import android.os.Bundle;
+
+/**
+ * Provides an interface matching
+ * {@link android.view.inputmethod.InputConnection#performPrivateCommand(String,Bundle)}.
+ */
+public interface PrivateCommandPerformer {
+ /**
+ * API to send private commands from an input method to its connected
+ * editor. This can be used to provide domain-specific features that are
+ * only known between certain input methods and their clients.
+ *
+ * @param action Name of the command to be performed. This must be a scoped
+ * name, i.e. prefixed with a package name you own, so that
+ * different developers will not create conflicting commands.
+ * @param data Any data to include with the command.
+ * @return true if the command was sent (regardless of whether the
+ * associated editor understood it), false if the input connection is no
+ * longer valid.
+ */
+ boolean performPrivateCommand(String action, Bundle data);
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java b/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java
index df447fd75..644818ba6 100644
--- a/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java
+++ b/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java
@@ -38,15 +38,13 @@ public final class DictionaryHeader {
public static final String DICTIONARY_DATE_KEY = "date";
public static final String HAS_HISTORICAL_INFO_KEY = "HAS_HISTORICAL_INFO";
public static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE";
- public static final String FORGETTING_CURVE_OCCURRENCES_TO_LEVEL_UP_KEY =
- "FORGETTING_CURVE_OCCURRENCES_TO_LEVEL_UP";
public static final String FORGETTING_CURVE_PROBABILITY_VALUES_TABLE_ID_KEY =
"FORGETTING_CURVE_PROBABILITY_VALUES_TABLE_ID";
- public static final String FORGETTING_CURVE_DURATION_TO_LEVEL_DOWN_IN_SECONDS_KEY =
- "FORGETTING_CURVE_DURATION_TO_LEVEL_DOWN_IN_SECONDS";
- public static final String MAX_UNIGRAM_COUNT_KEY = "MAX_UNIGRAM_COUNT";
- public static final String MAX_BIGRAM_COUNT_KEY = "MAX_BIGRAM_COUNT";
+ public static final String MAX_UNIGRAM_COUNT_KEY = "MAX_UNIGRAM_ENTRY_COUNT";
+ public static final String MAX_BIGRAM_COUNT_KEY = "MAX_BIGRAM_ENTRY_COUNT";
+ public static final String MAX_TRIGRAM_COUNT_KEY = "MAX_TRIGRAM_ENTRY_COUNT";
public static final String ATTRIBUTE_VALUE_TRUE = "1";
+ public static final String CODE_POINT_TABLE_KEY = "codePointTable";
public DictionaryHeader(final int headerSize, final DictionaryOptions dictionaryOptions,
final FormatOptions formatOptions) throws UnsupportedFormatException {
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
index a2ae74b20..4ef504856 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -17,7 +17,7 @@
package com.android.inputmethod.latin.makedict;
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.common.Constants;
import java.util.Date;
import java.util.HashMap;
@@ -36,9 +36,7 @@ public final class FormatSpec {
* sion
*
* o |
- * p | not used 3 bits
- * t | each unigram and bigram entry has a time stamp?
- * i | 1 bit, 1 = yes, 0 = no : CONTAINS_TIMESTAMP_FLAG
+ * p | not used, 2 bytes.
* o |
* nflags
*
@@ -48,7 +46,7 @@ public final class FormatSpec {
* d |
* ersize
*
- * | attributes list
+ * attributes list
*
* attributes list is:
* <key> = | string of characters at the char format described below, with the terminator used
@@ -86,27 +84,16 @@ public final class FormatSpec {
*/
/* Node (FusionDictionary.PtNode) layout is as follows:
- * | is moved ? 2 bits, 11 = no : FLAG_IS_NOT_MOVED
- * | This must be the same as FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES
- * | 01 = yes : FLAG_IS_MOVED
- * f | the new address is stored in the same place as the parent address
- * l | is deleted? 10 = yes : FLAG_IS_DELETED
+ * | CHILDREN_ADDRESS_TYPE 2 bits, 11 : FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES
+ * | 10 : FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES
+ * f | 01 : FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE
+ * l | 00 : FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS
* a | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS
* g | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL
* s | has shortcut targets ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_SHORTCUT_TARGETS
* | has bigrams ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_BIGRAMS
* | is not a word ? 1 bit, 1 = yes, 0 = no : FLAG_IS_NOT_A_WORD
- * | is blacklisted ? 1 bit, 1 = yes, 0 = no : FLAG_IS_BLACKLISTED
- *
- * p |
- * a | parent address, 3byte
- * r | 1 byte = bbbbbbbb match
- * e | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte)
- * n | otherwise => (bbbbbbbb << 16) + (next byte << 8) + next byte
- * t | This address is relative to the head of the PtNode.
- * a | If the node doesn't have a parent, this field is set to 0.
- * d |
- * dress
+ * | is possibly offensive ? 1 bit, 1 = yes, 0 = no : FLAG_IS_POSSIBLY_OFFENSIVE
*
* c | IF FLAG_HAS_MULTIPLE_CHARS
* h | char, char, char, char n * (1 or 3 bytes) : use PtNodeInfo for i/o helpers
@@ -121,15 +108,10 @@ public final class FormatSpec {
* q |
*
* c |
- * h | children address, 3 bytes
- * i | 1 byte = bbbbbbbb match
- * l | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte)
- * d | otherwise => (bbbbbbbb<<16) + (next byte << 8) + next byte
- * r | if this node doesn't have children, this field is set to 0.
- * e | (see BinaryDictEncoderUtils#writeVariableSignedAddress)
- * n | This address is relative to the position of this field.
- * a |
- * ddress
+ * h | children address, CHILDREN_ADDRESS_TYPE bytes
+ * i | This address is relative to the position of this field.
+ * l |
+ * drenaddress
*
* | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS
* | shortcut string list
@@ -179,32 +161,33 @@ public final class FormatSpec {
public static final int MAGIC_NUMBER = 0x9BC13AFE;
static final int NOT_A_VERSION_NUMBER = -1;
- static final int FIRST_VERSION_WITH_DYNAMIC_UPDATE = 3;
- static final int FIRST_VERSION_WITH_TERMINAL_ID = 4;
// These MUST have the same values as the relevant constants in format_utils.h.
- // From version 4 on, we use version * 100 + revision as a version number. That allows
+ // From version 2.01 on, we use version * 100 + revision as a version number. That allows
// us to change the format during development while having testing devices remove
// older files with each upgrade, while still having a readable versioning scheme.
// When we bump up the dictionary format version, we should update
// ExpandableDictionary.needsToMigrateDictionary() and
// ExpandableDictionary.matchesExpectedBinaryDictFormatVersionForThisType().
public static final int VERSION2 = 2;
+ public static final int VERSION201 = 201;
+ public static final int VERSION202 = 202;
+ public static final int MINIMUM_SUPPORTED_VERSION_OF_CODE_POINT_TABLE = VERSION201;
// Dictionary version used for testing.
public static final int VERSION4_ONLY_FOR_TESTING = 399;
- public static final int VERSION401 = 401;
- public static final int VERSION4 = 402;
- public static final int VERSION4_DEV = 403;
- static final int MINIMUM_SUPPORTED_VERSION = VERSION2;
- static final int MAXIMUM_SUPPORTED_VERSION = VERSION4_DEV;
+ public static final int VERSION402 = 402;
+ public static final int VERSION403 = 403;
+ public static final int VERSION4 = VERSION403;
+ public static final int VERSION4_DEV = VERSION403;
+ static final int MINIMUM_SUPPORTED_STATIC_VERSION = VERSION202;
+ static final int MAXIMUM_SUPPORTED_STATIC_VERSION = VERSION202;
+ static final int MINIMUM_SUPPORTED_DYNAMIC_VERSION = VERSION4;
+ static final int MAXIMUM_SUPPORTED_DYNAMIC_VERSION = VERSION4_DEV;
// TODO: Make this value adaptative to content data, store it in the header, and
// use it in the reading code.
static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
- static final int PARENT_ADDRESS_SIZE = 3;
- static final int FORWARD_LINK_ADDRESS_SIZE = 3;
-
// These flags are used only in the static dictionary.
static final int MASK_CHILDREN_ADDRESS_TYPE = 0xC0;
static final int FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS = 0x00;
@@ -218,14 +201,7 @@ public final class FormatSpec {
static final int FLAG_HAS_SHORTCUT_TARGETS = 0x08;
static final int FLAG_HAS_BIGRAMS = 0x04;
static final int FLAG_IS_NOT_A_WORD = 0x02;
- static final int FLAG_IS_BLACKLISTED = 0x01;
-
- // These flags are used only in the dynamic dictionary.
- static final int MASK_MOVE_AND_DELETE_FLAG = 0xC0;
- static final int FIXED_BIT_OF_DYNAMIC_UPDATE_MOVE = 0x40;
- static final int FLAG_IS_MOVED = 0x00 | FIXED_BIT_OF_DYNAMIC_UPDATE_MOVE;
- static final int FLAG_IS_NOT_MOVED = 0x80 | FIXED_BIT_OF_DYNAMIC_UPDATE_MOVE;
- static final int FLAG_IS_DELETED = 0x80;
+ static final int FLAG_IS_POSSIBLY_OFFENSIVE = 0x01;
static final int FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT = 0x80;
static final int FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE = 0x40;
@@ -240,52 +216,12 @@ public final class FormatSpec {
static final int PTNODE_TERMINATOR_SIZE = 1;
static final int PTNODE_FLAGS_SIZE = 1;
static final int PTNODE_FREQUENCY_SIZE = 1;
- static final int PTNODE_TERMINAL_ID_SIZE = 4;
static final int PTNODE_MAX_ADDRESS_SIZE = 3;
static final int PTNODE_ATTRIBUTE_FLAGS_SIZE = 1;
static final int PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
static final int PTNODE_SHORTCUT_LIST_SIZE_SIZE = 2;
- // These values are used only by version 4 or later. They MUST match the definitions in
- // ver4_dict_constants.cpp.
- static final String TRIE_FILE_EXTENSION = ".trie";
- public static final String HEADER_FILE_EXTENSION = ".header";
- static final String FREQ_FILE_EXTENSION = ".freq";
- // tat = Terminal Address Table
- static final String TERMINAL_ADDRESS_TABLE_FILE_EXTENSION = ".tat";
- static final String BIGRAM_FILE_EXTENSION = ".bigram";
- static final String SHORTCUT_FILE_EXTENSION = ".shortcut";
- static final String LOOKUP_TABLE_FILE_SUFFIX = "_lookup";
- static final String CONTENT_TABLE_FILE_SUFFIX = "_index";
- static final int FLAGS_IN_FREQ_FILE_SIZE = 1;
- static final int FREQUENCY_AND_FLAGS_SIZE = 2;
- static final int TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE = 3;
- static final int UNIGRAM_TIMESTAMP_SIZE = 4;
- static final int UNIGRAM_COUNTER_SIZE = 1;
- static final int UNIGRAM_LEVEL_SIZE = 1;
-
- // With the English main dictionary as of October 2013, the size of bigram address table is
- // is 345KB with the block size being 16.
- // This is 54% of that of full address table.
- static final int BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 16;
- static final int BIGRAM_CONTENT_COUNT = 1;
- static final int BIGRAM_FREQ_CONTENT_INDEX = 0;
- static final String BIGRAM_FREQ_CONTENT_ID = "_freq";
- static final int BIGRAM_TIMESTAMP_SIZE = 4;
- static final int BIGRAM_COUNTER_SIZE = 1;
- static final int BIGRAM_LEVEL_SIZE = 1;
-
- static final int SHORTCUT_CONTENT_COUNT = 1;
- static final int SHORTCUT_CONTENT_INDEX = 0;
- // With the English main dictionary as of October 2013, the size of shortcut address table is
- // 26KB with the block size being 64.
- // This is only 4.4% of that of full address table.
- static final int SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE = 64;
- static final String SHORTCUT_CONTENT_ID = "_shortcut";
-
static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
- static final int NO_PARENT_ADDRESS = 0;
- static final int NO_FORWARD_LINK_ADDRESS = 0;
static final int INVALID_CHARACTER = -1;
static final int MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT = 0x7F; // 127
@@ -302,14 +238,13 @@ public final class FormatSpec {
// This option needs to be the same numeric value as the one in binary_format.h.
static final int NOT_VALID_WORD = -99;
- static final int SIGNED_CHILDREN_ADDRESS_SIZE = 3;
static final int UINT8_MAX = 0xFF;
static final int UINT16_MAX = 0xFFFF;
static final int UINT24_MAX = 0xFFFFFF;
- static final int SINT24_MAX = 0x7FFFFF;
static final int MSB8 = 0x80;
- static final int MSB24 = 0x800000;
+ static final int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20;
+ static final int MAXIMAL_ONE_BYTE_CHARACTER_VALUE = 0xFF;
/**
* Options about file format.
diff --git a/java/src/com/android/inputmethod/latin/makedict/NgramProperty.java b/java/src/com/android/inputmethod/latin/makedict/NgramProperty.java
new file mode 100644
index 000000000..b1d19dc3c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/NgramProperty.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2014 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.makedict;
+
+import com.android.inputmethod.latin.NgramContext;
+
+public class NgramProperty {
+ public final WeightedString mTargetWord;
+ public final NgramContext mNgramContext;
+
+ public NgramProperty(final WeightedString targetWord, final NgramContext ngramContext) {
+ mTargetWord = targetWord;
+ mNgramContext = ngramContext;
+ }
+
+ @Override
+ public int hashCode() {
+ return mTargetWord.hashCode() ^ mNgramContext.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if (!(o instanceof NgramProperty)) return false;
+ final NgramProperty n = (NgramProperty)o;
+ return mTargetWord.equals(n.mTargetWord) && mNgramContext.equals(n.mNgramContext);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java b/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java
index 5fcbb6357..03c2ece1d 100644
--- a/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java
+++ b/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java
@@ -40,11 +40,8 @@ public final class ProbabilityInfo {
if (probabilityInfo2 == null) {
return probabilityInfo1;
}
- if (probabilityInfo1.mProbability > probabilityInfo2.mProbability) {
- return probabilityInfo1;
- } else {
- return probabilityInfo2;
- }
+ return (probabilityInfo1.mProbability > probabilityInfo2.mProbability) ? probabilityInfo1
+ : probabilityInfo2;
}
public ProbabilityInfo(final int probability) {
@@ -67,9 +64,8 @@ public final class ProbabilityInfo {
public int hashCode() {
if (hasHistoricalInfo()) {
return Arrays.hashCode(new Object[] { mProbability, mTimestamp, mLevel, mCount });
- } else {
- return Arrays.hashCode(new Object[] { mProbability });
}
+ return Arrays.hashCode(new Object[] { mProbability });
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/makedict/WordProperty.java b/java/src/com/android/inputmethod/latin/makedict/WordProperty.java
index cd78e2235..388d57816 100644
--- a/java/src/com/android/inputmethod/latin/makedict/WordProperty.java
+++ b/java/src/com/android/inputmethod/latin/makedict/WordProperty.java
@@ -18,12 +18,17 @@ package com.android.inputmethod.latin.makedict;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.NgramContext;
+import com.android.inputmethod.latin.NgramContext.WordInfo;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.utils.CombinedFormatUtils;
-import com.android.inputmethod.latin.utils.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
+import javax.annotation.Nullable;
+
/**
* Utility class for a word with a probability.
*
@@ -33,29 +38,38 @@ public final class WordProperty implements Comparable<WordProperty> {
public final String mWord;
public final ProbabilityInfo mProbabilityInfo;
public final ArrayList<WeightedString> mShortcutTargets;
- public final ArrayList<WeightedString> mBigrams;
+ public final ArrayList<NgramProperty> mNgrams;
// TODO: Support mIsBeginningOfSentence.
public final boolean mIsBeginningOfSentence;
public final boolean mIsNotAWord;
- public final boolean mIsBlacklistEntry;
+ public final boolean mIsPossiblyOffensive;
public final boolean mHasShortcuts;
- public final boolean mHasBigrams;
+ public final boolean mHasNgrams;
private int mHashCode = 0;
+ // TODO: Support n-gram.
@UsedForTesting
public WordProperty(final String word, final ProbabilityInfo probabilityInfo,
final ArrayList<WeightedString> shortcutTargets,
- final ArrayList<WeightedString> bigrams,
- final boolean isNotAWord, final boolean isBlacklistEntry) {
+ @Nullable final ArrayList<WeightedString> bigrams,
+ final boolean isNotAWord, final boolean isPossiblyOffensive) {
mWord = word;
mProbabilityInfo = probabilityInfo;
mShortcutTargets = shortcutTargets;
- mBigrams = bigrams;
+ if (null == bigrams) {
+ mNgrams = null;
+ } else {
+ mNgrams = new ArrayList<>();
+ final NgramContext ngramContext = new NgramContext(new WordInfo(mWord));
+ for (final WeightedString bigramTarget : bigrams) {
+ mNgrams.add(new NgramProperty(bigramTarget, ngramContext));
+ }
+ }
mIsBeginningOfSentence = false;
mIsNotAWord = isNotAWord;
- mIsBlacklistEntry = isBlacklistEntry;
- mHasBigrams = bigrams != null && !bigrams.isEmpty();
+ mIsPossiblyOffensive = isPossiblyOffensive;
+ mHasNgrams = bigrams != null && !bigrams.isEmpty();
mHasShortcuts = shortcutTargets != null && !shortcutTargets.isEmpty();
}
@@ -70,28 +84,43 @@ public final class WordProperty implements Comparable<WordProperty> {
// Construct word property using information from native code.
// This represents invalid word when the probability is BinaryDictionary.NOT_A_PROBABILITY.
public WordProperty(final int[] codePoints, final boolean isNotAWord,
- final boolean isBlacklisted, final boolean hasBigram, final boolean hasShortcuts,
+ final boolean isPossiblyOffensive, final boolean hasBigram, final boolean hasShortcuts,
final boolean isBeginningOfSentence, final int[] probabilityInfo,
- final ArrayList<int[]> bigramTargets, final ArrayList<int[]> bigramProbabilityInfo,
+ final ArrayList<int[][]> ngramPrevWordsArray,
+ final ArrayList<boolean[]> ngramPrevWordIsBeginningOfSentenceArray,
+ final ArrayList<int[]> ngramTargets, final ArrayList<int[]> ngramProbabilityInfo,
final ArrayList<int[]> shortcutTargets,
final ArrayList<Integer> shortcutProbabilities) {
mWord = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints);
mProbabilityInfo = createProbabilityInfoFromArray(probabilityInfo);
mShortcutTargets = new ArrayList<>();
- mBigrams = new ArrayList<>();
+ final ArrayList<NgramProperty> ngrams = new ArrayList<>();
mIsBeginningOfSentence = isBeginningOfSentence;
mIsNotAWord = isNotAWord;
- mIsBlacklistEntry = isBlacklisted;
+ mIsPossiblyOffensive = isPossiblyOffensive;
mHasShortcuts = hasShortcuts;
- mHasBigrams = hasBigram;
-
- final int bigramTargetCount = bigramTargets.size();
- for (int i = 0; i < bigramTargetCount; i++) {
- final String bigramTargetString =
- StringUtils.getStringFromNullTerminatedCodePointArray(bigramTargets.get(i));
- mBigrams.add(new WeightedString(bigramTargetString,
- createProbabilityInfoFromArray(bigramProbabilityInfo.get(i))));
+ mHasNgrams = hasBigram;
+
+ final int relatedNgramCount = ngramTargets.size();
+ for (int i = 0; i < relatedNgramCount; i++) {
+ final String ngramTargetString =
+ StringUtils.getStringFromNullTerminatedCodePointArray(ngramTargets.get(i));
+ final WeightedString ngramTarget = new WeightedString(ngramTargetString,
+ createProbabilityInfoFromArray(ngramProbabilityInfo.get(i)));
+ final int[][] prevWords = ngramPrevWordsArray.get(i);
+ final boolean[] isBeginningOfSentenceArray =
+ ngramPrevWordIsBeginningOfSentenceArray.get(i);
+ final WordInfo[] wordInfoArray = new WordInfo[prevWords.length];
+ for (int j = 0; j < prevWords.length; j++) {
+ wordInfoArray[j] = isBeginningOfSentenceArray[j]
+ ? WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO
+ : new WordInfo(StringUtils.getStringFromNullTerminatedCodePointArray(
+ prevWords[j]));
+ }
+ final NgramContext ngramContext = new NgramContext(wordInfoArray);
+ ngrams.add(new NgramProperty(ngramTarget, ngramContext));
}
+ mNgrams = ngrams.isEmpty() ? null : ngrams;
final int shortcutTargetCount = shortcutTargets.size();
for (int i = 0; i < shortcutTargetCount; i++) {
@@ -102,6 +131,21 @@ public final class WordProperty implements Comparable<WordProperty> {
}
}
+ // TODO: Remove
+ @UsedForTesting
+ public ArrayList<WeightedString> getBigrams() {
+ if (null == mNgrams) {
+ return null;
+ }
+ final ArrayList<WeightedString> bigrams = new ArrayList<>();
+ for (final NgramProperty ngram : mNgrams) {
+ if (ngram.mNgramContext.getPrevWordCount() == 1) {
+ bigrams.add(ngram.mTargetWord);
+ }
+ }
+ return bigrams;
+ }
+
public int getProbability() {
return mProbabilityInfo.mProbability;
}
@@ -110,10 +154,10 @@ public final class WordProperty implements Comparable<WordProperty> {
return Arrays.hashCode(new Object[] {
word.mWord,
word.mProbabilityInfo,
- word.mShortcutTargets.hashCode(),
- word.mBigrams.hashCode(),
+ word.mShortcutTargets,
+ word.mNgrams,
word.mIsNotAWord,
- word.mIsBlacklistEntry
+ word.mIsPossiblyOffensive
});
}
@@ -142,9 +186,17 @@ public final class WordProperty implements Comparable<WordProperty> {
if (!(o instanceof WordProperty)) return false;
WordProperty w = (WordProperty)o;
return mProbabilityInfo.equals(w.mProbabilityInfo) && mWord.equals(w.mWord)
- && mShortcutTargets.equals(w.mShortcutTargets) && mBigrams.equals(w.mBigrams)
- && mIsNotAWord == w.mIsNotAWord && mIsBlacklistEntry == w.mIsBlacklistEntry
- && mHasBigrams == w.mHasBigrams && mHasShortcuts && w.mHasBigrams;
+ && mShortcutTargets.equals(w.mShortcutTargets) && equals(mNgrams, w.mNgrams)
+ && mIsNotAWord == w.mIsNotAWord && mIsPossiblyOffensive == w.mIsPossiblyOffensive
+ && mHasNgrams == w.mHasNgrams && mHasShortcuts && w.mHasNgrams;
+ }
+
+ // TDOO: Have a utility method like java.util.Objects.equals.
+ private static <T> boolean equals(final ArrayList<T> a, final ArrayList<T> b) {
+ if (null == a) {
+ return null == b;
+ }
+ return a.equals(b);
}
@Override
@@ -157,7 +209,7 @@ public final class WordProperty implements Comparable<WordProperty> {
@UsedForTesting
public boolean isValid() {
- return getProbability() != BinaryDictionary.NOT_A_PROBABILITY;
+ return getProbability() != Dictionary.NOT_A_PROBABILITY;
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/network/AuthException.java b/java/src/com/android/inputmethod/latin/network/AuthException.java
new file mode 100644
index 000000000..1bce4c156
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/network/AuthException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 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.network;
+
+/**
+ * Authentication exception. When this exception is thrown, the client may
+ * try to refresh the authentication token and try again.
+ */
+public class AuthException extends Exception {
+ public AuthException() {
+ super();
+ }
+
+ public AuthException(Throwable throwable) {
+ super(throwable);
+ }
+
+ public AuthException(String detailMessage) {
+ super(detailMessage);
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/network/BlockingHttpClient.java b/java/src/com/android/inputmethod/latin/network/BlockingHttpClient.java
new file mode 100644
index 000000000..079d07eac
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/network/BlockingHttpClient.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2014 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.network;
+
+import android.util.Log;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * A client for executing HTTP requests synchronously.
+ * This must never be called from the main thread.
+ */
+public class BlockingHttpClient {
+ private static final boolean DEBUG = false;
+ private static final String TAG = BlockingHttpClient.class.getSimpleName();
+
+ private final HttpURLConnection mConnection;
+
+ /**
+ * Interface that handles processing the response for a request.
+ */
+ public interface ResponseProcessor<T> {
+ /**
+ * Called when the HTTP request finishes successfully.
+ * The {@link InputStream} is closed by the client after the method finishes,
+ * so any processing must be done in this method itself.
+ *
+ * @param response An input stream that can be used to read the HTTP response.
+ */
+ T onSuccess(InputStream response) throws IOException;
+ }
+
+ public BlockingHttpClient(HttpURLConnection connection) {
+ mConnection = connection;
+ }
+
+ /**
+ * Executes the request on the underlying {@link HttpURLConnection}.
+ *
+ * @param request The request payload, if any, or null.
+ * @param responseProcessor A processor for the HTTP response.
+ */
+ public <T> T execute(@Nullable byte[] request, @Nonnull ResponseProcessor<T> responseProcessor)
+ throws IOException, AuthException, HttpException {
+ if (DEBUG) {
+ Log.d(TAG, "execute: " + mConnection.getURL());
+ }
+ try {
+ if (request != null) {
+ if (DEBUG) {
+ Log.d(TAG, "request size: " + request.length);
+ }
+ OutputStream out = new BufferedOutputStream(mConnection.getOutputStream());
+ out.write(request);
+ out.flush();
+ out.close();
+ }
+
+ final int responseCode = mConnection.getResponseCode();
+ if (responseCode != HttpURLConnection.HTTP_OK) {
+ Log.w(TAG, "Response error: " + responseCode + ", Message: "
+ + mConnection.getResponseMessage());
+ if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ throw new AuthException(mConnection.getResponseMessage());
+ }
+ throw new HttpException(responseCode);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "request executed successfully");
+ }
+ return responseProcessor.onSuccess(mConnection.getInputStream());
+ } finally {
+ mConnection.disconnect();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/network/HttpException.java b/java/src/com/android/inputmethod/latin/network/HttpException.java
new file mode 100644
index 000000000..b9d8b6372
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/network/HttpException.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 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.network;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+/**
+ * The HttpException exception represents a XML/HTTP fault with a HTTP status code.
+ */
+public class HttpException extends Exception {
+
+ /**
+ * The HTTP status code.
+ */
+ private final int mStatusCode;
+
+ /**
+ * @param statusCode int HTTP status code.
+ */
+ public HttpException(int statusCode) {
+ super("Response Code: " + statusCode);
+ mStatusCode = statusCode;
+ }
+
+ /**
+ * @return the HTTP status code related to this exception.
+ */
+ @UsedForTesting
+ public int getHttpStatusCode() {
+ return mStatusCode;
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/network/HttpUrlConnectionBuilder.java b/java/src/com/android/inputmethod/latin/network/HttpUrlConnectionBuilder.java
new file mode 100644
index 000000000..df54bf464
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/network/HttpUrlConnectionBuilder.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2014 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.network;
+
+import android.text.TextUtils;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+/**
+ * Builder for {@link HttpURLConnection}s.
+ *
+ * TODO: Remove @UsedForTesting after this is actually used.
+ */
+@UsedForTesting
+public class HttpUrlConnectionBuilder {
+ private static final int DEFAULT_TIMEOUT_MILLIS = 5 * 1000;
+
+ /**
+ * Request header key for authentication.
+ */
+ public static final String HTTP_HEADER_AUTHORIZATION = "Authorization";
+
+ /**
+ * Request header key for cache control.
+ */
+ public static final String KEY_CACHE_CONTROL = "Cache-Control";
+ /**
+ * Request header value for cache control indicating no caching.
+ * @see #KEY_CACHE_CONTROL
+ */
+ public static final String VALUE_NO_CACHE = "no-cache";
+
+ /**
+ * Indicates that the request is unidirectional - upload-only.
+ * TODO: Remove @UsedForTesting after this is actually used.
+ */
+ @UsedForTesting
+ public static final int MODE_UPLOAD_ONLY = 1;
+ /**
+ * Indicates that the request is unidirectional - download only.
+ * TODO: Remove @UsedForTesting after this is actually used.
+ */
+ @UsedForTesting
+ public static final int MODE_DOWNLOAD_ONLY = 2;
+ /**
+ * Indicates that the request is bi-directional.
+ * TODO: Remove @UsedForTesting after this is actually used.
+ */
+ @UsedForTesting
+ public static final int MODE_BI_DIRECTIONAL = 3;
+
+ private final HashMap<String, String> mHeaderMap = new HashMap<>();
+
+ private URL mUrl;
+ private int mConnectTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
+ private int mReadTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
+ private int mContentLength = -1;
+ private boolean mUseCache;
+ private int mMode;
+
+ /**
+ * Sets the URL that'll be used for the request.
+ * This *must* be set before calling {@link #build()}
+ *
+ * TODO: Remove @UsedForTesting after this method is actually used.
+ */
+ @UsedForTesting
+ public HttpUrlConnectionBuilder setUrl(String url) throws MalformedURLException {
+ if (TextUtils.isEmpty(url)) {
+ throw new IllegalArgumentException("URL must not be empty");
+ }
+ mUrl = new URL(url);
+ return this;
+ }
+
+ /**
+ * Sets the connect timeout. Defaults to {@value #DEFAULT_TIMEOUT_MILLIS} milliseconds.
+ *
+ * TODO: Remove @UsedForTesting after this method is actually used.
+ */
+ @UsedForTesting
+ public HttpUrlConnectionBuilder setConnectTimeout(int timeoutMillis) {
+ if (timeoutMillis < 0) {
+ throw new IllegalArgumentException("connect-timeout must be >= 0, but was "
+ + timeoutMillis);
+ }
+ mConnectTimeoutMillis = timeoutMillis;
+ return this;
+ }
+
+ /**
+ * Sets the read timeout. Defaults to {@value #DEFAULT_TIMEOUT_MILLIS} milliseconds.
+ *
+ * TODO: Remove @UsedForTesting after this method is actually used.
+ */
+ @UsedForTesting
+ public HttpUrlConnectionBuilder setReadTimeout(int timeoutMillis) {
+ if (timeoutMillis < 0) {
+ throw new IllegalArgumentException("read-timeout must be >= 0, but was "
+ + timeoutMillis);
+ }
+ mReadTimeoutMillis = timeoutMillis;
+ return this;
+ }
+
+ /**
+ * Adds an entry to the request header.
+ *
+ * TODO: Remove @UsedForTesting after this method is actually used.
+ */
+ @UsedForTesting
+ public HttpUrlConnectionBuilder addHeader(String key, String value) {
+ mHeaderMap.put(key, value);
+ return this;
+ }
+
+ /**
+ * Sets an authentication token.
+ *
+ * TODO: Remove @UsedForTesting after this method is actually used.
+ */
+ @UsedForTesting
+ public HttpUrlConnectionBuilder setAuthToken(String value) {
+ mHeaderMap.put(HTTP_HEADER_AUTHORIZATION, value);
+ return this;
+ }
+
+ /**
+ * Sets the request to be executed such that the input is not buffered.
+ * This may be set when the request size is known beforehand.
+ *
+ * TODO: Remove @UsedForTesting after this method is actually used.
+ */
+ @UsedForTesting
+ public HttpUrlConnectionBuilder setFixedLengthForStreaming(int length) {
+ mContentLength = length;
+ return this;
+ }
+
+ /**
+ * Indicates if the request can use cached responses or not.
+ *
+ * TODO: Remove @UsedForTesting after this method is actually used.
+ */
+ @UsedForTesting
+ public HttpUrlConnectionBuilder setUseCache(boolean useCache) {
+ mUseCache = useCache;
+ return this;
+ }
+
+ /**
+ * The request mode.
+ * Sets the request mode to be one of: upload-only, download-only or bidirectional.
+ *
+ * @see #MODE_UPLOAD_ONLY
+ * @see #MODE_DOWNLOAD_ONLY
+ * @see #MODE_BI_DIRECTIONAL
+ *
+ * TODO: Remove @UsedForTesting after this method is actually used
+ */
+ @UsedForTesting
+ public HttpUrlConnectionBuilder setMode(int mode) {
+ if (mode != MODE_UPLOAD_ONLY
+ && mode != MODE_DOWNLOAD_ONLY
+ && mode != MODE_BI_DIRECTIONAL) {
+ throw new IllegalArgumentException("Invalid mode specified:" + mode);
+ }
+ mMode = mode;
+ return this;
+ }
+
+ /**
+ * Builds the {@link HttpURLConnection} instance that can be used to execute the request.
+ *
+ * TODO: Remove @UsedForTesting after this method is actually used.
+ */
+ @UsedForTesting
+ public HttpURLConnection build() throws IOException {
+ if (mUrl == null) {
+ throw new IllegalArgumentException("A URL must be specified!");
+ }
+ final HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
+ connection.setConnectTimeout(mConnectTimeoutMillis);
+ connection.setReadTimeout(mReadTimeoutMillis);
+ connection.setUseCaches(mUseCache);
+ switch (mMode) {
+ case MODE_UPLOAD_ONLY:
+ connection.setDoInput(true);
+ connection.setDoOutput(false);
+ break;
+ case MODE_DOWNLOAD_ONLY:
+ connection.setDoInput(false);
+ connection.setDoOutput(true);
+ break;
+ case MODE_BI_DIRECTIONAL:
+ connection.setDoInput(true);
+ connection.setDoOutput(true);
+ break;
+ }
+ for (final Entry<String, String> entry : mHeaderMap.entrySet()) {
+ connection.addRequestProperty(entry.getKey(), entry.getValue());
+ }
+ if (mContentLength >= 0) {
+ connection.setFixedLengthStreamingMode(mContentLength);
+ }
+ return connection;
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/personalization/ContextualDictionary.java b/java/src/com/android/inputmethod/latin/personalization/ContextualDictionary.java
index ac55b9333..39d9596ef 100644
--- a/java/src/com/android/inputmethod/latin/personalization/ContextualDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/ContextualDictionary.java
@@ -18,7 +18,7 @@ package com.android.inputmethod.latin.personalization;
import android.content.Context;
-import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.annotations.ExternallyReferenced;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.ExpandableBinaryDictionary;
@@ -36,7 +36,9 @@ public class ContextualDictionary extends ExpandableBinaryDictionary {
clear();
}
- @UsedForTesting
+ // Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
+ @SuppressWarnings("unused")
+ @ExternallyReferenced
public static ContextualDictionary getDictionary(final Context context, final Locale locale,
final File dictFile, final String dictNamePrefix) {
return new ContextualDictionary(context, locale, dictFile);
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
index 1ba7b366f..78b51d9f4 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -39,14 +39,10 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB
public static final int FREQUENCY_FOR_WORDS_IN_DICTS = FREQUENCY_FOR_TYPED;
public static final int FREQUENCY_FOR_WORDS_NOT_IN_DICTS = Dictionary.NOT_A_PROBABILITY;
- /** The locale for this dictionary. */
- public final Locale mLocale;
-
protected DecayingExpandableBinaryDictionaryBase(final Context context,
final String dictName, final Locale locale, final String dictionaryType,
final File dictFile) {
super(context, dictName, locale, dictionaryType, dictFile);
- mLocale = locale;
if (mLocale != null && mLocale.toString().length() > 1) {
reloadDictionaryIfRequired();
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java
index 9d72de8c5..734ed5583 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java
@@ -18,20 +18,22 @@ package com.android.inputmethod.latin.personalization;
import java.util.Collections;
import java.util.List;
-import java.util.Locale;
public class PersonalizationDataChunk {
+ public static final String LANGUAGE_UNKNOWN = "";
+
public final boolean mInputByUser;
public final List<String> mTokens;
public final int mTimestampInSeconds;
public final String mPackageName;
- public final Locale mlocale = null;
+ public final String mDetectedLanguage;
public PersonalizationDataChunk(boolean inputByUser, final List<String> tokens,
- final int timestampInSeconds, final String packageName) {
+ final int timestampInSeconds, final String packageName, final String detectedLanguage) {
mInputByUser = inputByUser;
mTokens = Collections.unmodifiableList(tokens);
mTimestampInSeconds = timestampInSeconds;
mPackageName = packageName;
+ mDetectedLanguage = detectedLanguage;
}
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
index f2ad22ac7..33d1273f7 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
@@ -18,7 +18,7 @@ package com.android.inputmethod.latin.personalization;
import android.content.Context;
-import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.annotations.ExternallyReferenced;
import com.android.inputmethod.latin.Dictionary;
import java.io.File;
@@ -33,7 +33,9 @@ public class PersonalizationDictionary extends DecayingExpandableBinaryDictionar
Dictionary.TYPE_PERSONALIZATION, null /* dictFile */);
}
- @UsedForTesting
+ // Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
+ @SuppressWarnings("unused")
+ @ExternallyReferenced
public static PersonalizationDictionary getDictionary(final Context context,
final Locale locale, final File dictFile, final String dictNamePrefix) {
return PersonalizationHelper.getPersonalizationDictionary(context, locale);
diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
index 34d4d4ed7..58782c646 100644
--- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
@@ -17,18 +17,19 @@
package com.android.inputmethod.latin.personalization;
import android.content.Context;
-import android.text.TextUtils;
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.annotations.ExternallyReferenced;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.ExpandableBinaryDictionary;
-import com.android.inputmethod.latin.PrevWordsInfo;
+import com.android.inputmethod.latin.NgramContext;
+import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.utils.DistracterFilter;
import java.io.File;
import java.util.Locale;
+import javax.annotation.Nonnull;
+
/**
* Locally gathers stats about the words user types and various other signals like auto-correction
* cancellation or manual picks. This allows the keyboard to adapt to the typist over time.
@@ -42,7 +43,9 @@ public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBas
Dictionary.TYPE_USER_HISTORY, null /* dictFile */);
}
- @UsedForTesting
+ // Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
+ @SuppressWarnings("unused")
+ @ExternallyReferenced
public static UserHistoryDictionary getDictionary(final Context context, final Locale locale,
final File dictFile, final String dictNamePrefix) {
return PersonalizationHelper.getUserHistoryDictionary(context, locale);
@@ -52,37 +55,19 @@ public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBas
* Add a word to the user history dictionary.
*
* @param userHistoryDictionary the user history dictionary
- * @param prevWordsInfo the information of previous words
+ * @param ngramContext the n-gram context
* @param word the word the user inputted
* @param isValid whether the word is valid or not
* @param timestamp the timestamp when the word has been inputted
* @param distracterFilter the filter to check whether the word is a distracter
*/
public static void addToDictionary(final ExpandableBinaryDictionary userHistoryDictionary,
- final PrevWordsInfo prevWordsInfo, final String word, final boolean isValid,
- final int timestamp, final DistracterFilter distracterFilter) {
- final CharSequence prevWord = prevWordsInfo.mPrevWordsInfo[0].mWord;
- if (word.length() > Constants.DICTIONARY_MAX_WORD_LENGTH ||
- (prevWord != null && prevWord.length() > Constants.DICTIONARY_MAX_WORD_LENGTH)) {
- return;
- }
- final int frequency = isValid ?
- FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS;
- userHistoryDictionary.addUnigramEntryWithCheckingDistracter(word, frequency,
- null /* shortcutTarget */, 0 /* shortcutFreq */, false /* isNotAWord */,
- false /* isBlacklisted */, timestamp, distracterFilter);
- // Do not insert a word as a bigram of itself
- if (TextUtils.equals(word, prevWord)) {
+ @Nonnull final NgramContext ngramContext, final String word, final boolean isValid,
+ final int timestamp, @Nonnull final DistracterFilter distracterFilter) {
+ if (word.length() > Constants.DICTIONARY_MAX_WORD_LENGTH) {
return;
}
- if (null != prevWord) {
- if (prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence) {
- // Beginning-of-Sentence n-gram entry is treated as a n-gram entry of invalid word.
- userHistoryDictionary.addNgramEntry(prevWordsInfo, word,
- FREQUENCY_FOR_WORDS_NOT_IN_DICTS, timestamp);
- } else {
- userHistoryDictionary.addNgramEntry(prevWordsInfo, word, frequency, timestamp);
- }
- }
+ userHistoryDictionary.updateEntriesForWordWithCheckingDistracter(ngramContext, word,
+ isValid, 1 /* count */, timestamp, distracterFilter);
}
}
diff --git a/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java
new file mode 100644
index 000000000..4bd15d037
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2014 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.settings;
+
+import static com.android.inputmethod.latin.settings.LocalSettingsConstants.PREF_ACCOUNT_NAME;
+import static com.android.inputmethod.latin.settings.LocalSettingsConstants.PREF_ENABLE_CLOUD_SYNC;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.text.TextUtils;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.accounts.LoginAccountUtils;
+import com.android.inputmethod.latin.accounts.AccountStateChangedListener;
+import com.android.inputmethod.latin.define.ProductionFlags;
+
+import javax.annotation.Nullable;
+
+/**
+ * "Accounts & Privacy" settings sub screen.
+ *
+ * This settings sub screen handles the following preferences:
+ * <li> Account selection/management for IME </li>
+ * <li> Sync preferences </li>
+ * <li> Privacy preferences </li>
+ */
+public final class AccountsSettingsFragment extends SubScreenFragment {
+ private static final String PREF_SYNC_NOW = "pref_beanstalk";
+
+ static final String PREF_ACCCOUNT_SWITCHER = "account_switcher";
+
+ private final DialogInterface.OnClickListener mAccountChangedListener =
+ new AccountChangedListener();
+ private final Preference.OnPreferenceClickListener mSyncNowListener = new SyncNowListener();
+
+ @Override
+ public void onCreate(final Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.prefs_screen_accounts);
+
+ final Resources res = getResources();
+ final Context context = getActivity();
+
+ // When we are called from the Settings application but we are not already running, some
+ // singleton and utility classes may not have been initialized. We have to call
+ // initialization method of these classes here. See {@link LatinIME#onCreate()}.
+ SubtypeSwitcher.init(context);
+
+ if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) {
+ final Preference enableMetricsLogging =
+ findPreference(Settings.PREF_ENABLE_METRICS_LOGGING);
+ if (enableMetricsLogging != null) {
+ final String enableMetricsLoggingTitle = res.getString(
+ R.string.enable_metrics_logging, getApplicationName());
+ enableMetricsLogging.setTitle(enableMetricsLoggingTitle);
+ }
+ } else {
+ removePreference(Settings.PREF_ENABLE_METRICS_LOGGING);
+ }
+
+ if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) {
+ removePreference(PREF_ACCCOUNT_SWITCHER);
+ removePreference(PREF_ENABLE_CLOUD_SYNC);
+ removePreference(PREF_SYNC_NOW);
+ }
+ if (!ProductionFlags.ENABLE_PERSONAL_DICTIONARY_SYNC) {
+ removePreference(PREF_ENABLE_CLOUD_SYNC);
+ removePreference(PREF_SYNC_NOW);
+ } else {
+ final Preference syncNowPreference = findPreference(PREF_SYNC_NOW);
+ syncNowPreference.setOnPreferenceClickListener(mSyncNowListener);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refreshAccountAndDependentPreferences(getSignedInAccountName());
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+ if (TextUtils.equals(key, PREF_ACCOUNT_NAME)) {
+ refreshAccountAndDependentPreferences(
+ prefs.getString(PREF_ACCOUNT_NAME, null));
+ } else if (TextUtils.equals(key, PREF_ENABLE_CLOUD_SYNC)) {
+ final boolean syncEnabled = prefs.getBoolean(PREF_ENABLE_CLOUD_SYNC, false);
+ AccountStateChangedListener.onSyncPreferenceChanged(
+ getSignedInAccountName(), syncEnabled);
+ }
+ }
+
+ private void refreshAccountAndDependentPreferences(@Nullable final String currentAccount) {
+ if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) {
+ return;
+ }
+
+ final Preference accountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER);
+ if (currentAccount == null) {
+ // No account is currently selected.
+ accountSwitcher.setSummary(getString(R.string.no_accounts_selected));
+ // Disable the sync preference UI.
+ disableSyncPreference();
+ } else {
+ // Set the currently selected account.
+ accountSwitcher.setSummary(getString(R.string.account_selected, currentAccount));
+ // Enable the sync preference UI.
+ enableSyncPreference();
+ }
+ // Set up onClick listener for the account picker preference.
+ final Context context = getActivity();
+ final String[] accountsForLogin = LoginAccountUtils.getAccountsForLogin(context);
+ accountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (accountsForLogin.length == 0) {
+ // TODO: Handle account addition.
+ Toast.makeText(getActivity(), getString(R.string.account_select_cancel),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ createAccountPicker(accountsForLogin, currentAccount).show();
+ }
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Enables the Sync preference UI and updates its summary.
+ */
+ private void enableSyncPreference() {
+ if (!ProductionFlags.ENABLE_PERSONAL_DICTIONARY_SYNC) {
+ return;
+ }
+
+ final Preference syncPreference = findPreference(PREF_ENABLE_CLOUD_SYNC);
+ syncPreference.setEnabled(true);
+ syncPreference.setSummary(R.string.cloud_sync_summary);
+ }
+
+ /**
+ * Disables the Sync preference UI and updates its summary to indicate
+ * the fact that an account needs to be selected for sync.
+ */
+ private void disableSyncPreference() {
+ if (!ProductionFlags.ENABLE_PERSONAL_DICTIONARY_SYNC) {
+ return;
+ }
+
+ final Preference syncPreference = findPreference(PREF_ENABLE_CLOUD_SYNC);
+ syncPreference.setEnabled(false);
+ syncPreference.setSummary(R.string.cloud_sync_summary_disabled_signed_out);
+ }
+
+ @Nullable
+ String getSignedInAccountName() {
+ return getSharedPreferences().getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null);
+ }
+
+ boolean isSyncEnabled() {
+ return getSharedPreferences().getBoolean(PREF_ENABLE_CLOUD_SYNC, false);
+ }
+
+ /**
+ * Creates an account picker dialog showing the given accounts in a list and selecting
+ * the selected account by default.
+ * The list of accounts must not be null/empty.
+ *
+ * Package-private for testing.
+ */
+ @UsedForTesting
+ AlertDialog createAccountPicker(final String[] accounts,
+ final String selectedAccount) {
+ if (accounts == null || accounts.length == 0) {
+ throw new IllegalArgumentException("List of accounts must not be empty");
+ }
+
+ // See if the currently selected account is in the list.
+ // If it is, the entry is selected, and a sign-out button is provided.
+ // If it isn't, select the 0th account by default which will get picked up
+ // if the user presses OK.
+ int index = 0;
+ boolean isSignedIn = false;
+ for (int i = 0; i < accounts.length; i++) {
+ if (TextUtils.equals(accounts[i], selectedAccount)) {
+ index = i;
+ isSignedIn = true;
+ break;
+ }
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.account_select_title)
+ .setSingleChoiceItems(accounts, index, null)
+ .setPositiveButton(R.string.account_select_ok, mAccountChangedListener)
+ .setNegativeButton(R.string.account_select_cancel, null);
+ if (isSignedIn) {
+ builder.setNeutralButton(R.string.account_select_sign_out, mAccountChangedListener);
+ }
+ return builder.create();
+ }
+
+ /**
+ * Listener for a account selection changes from the picker.
+ * Persists/removes the account to/from shared preferences and sets up sync if required.
+ */
+ class AccountChangedListener implements DialogInterface.OnClickListener {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final String oldAccount = getSignedInAccountName();
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE: // Signed in
+ final ListView lv = ((AlertDialog)dialog).getListView();
+ final String newAccount =
+ (String) lv.getItemAtPosition(lv.getCheckedItemPosition());
+ getSharedPreferences()
+ .edit()
+ .putString(PREF_ACCOUNT_NAME, newAccount)
+ .apply();
+ AccountStateChangedListener.onAccountSignedIn(oldAccount, newAccount);
+ break;
+ case DialogInterface.BUTTON_NEUTRAL: // Signed out
+ AccountStateChangedListener.onAccountSignedOut(oldAccount);
+ getSharedPreferences()
+ .edit()
+ .remove(PREF_ACCOUNT_NAME)
+ .apply();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Listener that initiates the process of sync in the background.
+ */
+ class SyncNowListener implements Preference.OnPreferenceClickListener {
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ AccountStateChangedListener.forceSync(getSignedInAccountName());
+ return true;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
index 00f2c73dd..d2c9dbbe9 100644
--- a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
@@ -93,22 +93,27 @@ public final class AdvancedSettingsFragment extends SubScreenFragment {
removePreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON);
}
+ // If metrics logging isn't supported, or account sign in is enabled
+ // don't show the logging preference.
+ // TODO: Eventually when we enable account sign in by default,
+ // we'll remove logging preference from here.
if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) {
final Preference enableMetricsLogging =
findPreference(Settings.PREF_ENABLE_METRICS_LOGGING);
if (enableMetricsLogging != null) {
- final int applicationLabelRes = context.getApplicationInfo().labelRes;
- final String applicationName = res.getString(applicationLabelRes);
final String enableMetricsLoggingTitle = res.getString(
- R.string.enable_metrics_logging, applicationName);
+ R.string.enable_metrics_logging, getApplicationName());
enableMetricsLogging.setTitle(enableMetricsLoggingTitle);
}
} else {
removePreference(Settings.PREF_ENABLE_METRICS_LOGGING);
}
+ AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(context, this);
+
setupKeypressVibrationDurationSettings();
setupKeypressSoundVolumeSettings();
+ setupKeyLongpressTimeoutSettings();
refreshEnablingsOfKeypressSoundAndVibrationSettings();
}
@@ -245,4 +250,43 @@ public final class AdvancedSettingsFragment extends SubScreenFragment {
}
});
}
+
+ private void setupKeyLongpressTimeoutSettings() {
+ final SharedPreferences prefs = getSharedPreferences();
+ final Resources res = getResources();
+ final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(
+ Settings.PREF_KEY_LONGPRESS_TIMEOUT);
+ if (pref == null) {
+ return;
+ }
+ pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
+ @Override
+ public void writeValue(final int value, final String key) {
+ prefs.edit().putInt(key, value).apply();
+ }
+
+ @Override
+ public void writeDefaultValue(final String key) {
+ prefs.edit().remove(key).apply();
+ }
+
+ @Override
+ public int readValue(final String key) {
+ return Settings.readKeyLongpressTimeout(prefs, res);
+ }
+
+ @Override
+ public int readDefaultValue(final String key) {
+ return Settings.readDefaultKeyLongpressTimeout(res);
+ }
+
+ @Override
+ public String getValueText(final int value) {
+ return res.getString(R.string.abbreviation_unit_milliseconds, value);
+ }
+
+ @Override
+ public void feedbackValue(final int value) {}
+ });
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java
index f5e4d33a2..554edc85c 100644
--- a/java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java
@@ -19,7 +19,8 @@ package com.android.inputmethod.latin.settings;
import android.os.Bundle;
import com.android.inputmethod.latin.R;
-
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.define.ProductionFlags;
/**
* "Appearance" settings sub screen.
@@ -29,6 +30,10 @@ public final class AppearanceSettingsFragment extends SubScreenFragment {
public void onCreate(final Bundle icicle) {
super.onCreate(icicle);
addPreferencesFromResource(R.xml.prefs_screen_appearance);
+ if (!ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED ||
+ Constants.isPhone(Settings.readScreenMetrics(getResources()))) {
+ removePreference(Settings.PREF_ENABLE_SPLIT_KEYBOARD);
+ }
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/settings/CustomInputStylePreference.java b/java/src/com/android/inputmethod/latin/settings/CustomInputStylePreference.java
new file mode 100644
index 000000000..01398f467
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/settings/CustomInputStylePreference.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2014 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.settings;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.DialogPreference;
+import android.preference.Preference;
+import android.util.Log;
+import android.view.View;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import android.widget.SpinnerAdapter;
+
+import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
+import com.android.inputmethod.compat.ViewCompatUtils;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.RichInputMethodManager;
+import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
+
+import java.util.TreeSet;
+
+final class CustomInputStylePreference extends DialogPreference
+ implements DialogInterface.OnCancelListener {
+ private static final boolean DEBUG_SUBTYPE_ID = false;
+
+ interface Listener {
+ public void onRemoveCustomInputStyle(CustomInputStylePreference stylePref);
+ public void onSaveCustomInputStyle(CustomInputStylePreference stylePref);
+ public void onAddCustomInputStyle(CustomInputStylePreference stylePref);
+ public SubtypeLocaleAdapter getSubtypeLocaleAdapter();
+ public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter();
+ }
+
+ private static final String KEY_PREFIX = "subtype_pref_";
+ private static final String KEY_NEW_SUBTYPE = KEY_PREFIX + "new";
+
+ private InputMethodSubtype mSubtype;
+ private InputMethodSubtype mPreviousSubtype;
+
+ private final Listener mProxy;
+ private Spinner mSubtypeLocaleSpinner;
+ private Spinner mKeyboardLayoutSetSpinner;
+
+ public static CustomInputStylePreference newIncompleteSubtypePreference(
+ final Context context, final Listener proxy) {
+ return new CustomInputStylePreference(context, null, proxy);
+ }
+
+ public CustomInputStylePreference(final Context context, final InputMethodSubtype subtype,
+ final Listener proxy) {
+ super(context, null);
+ setDialogLayoutResource(R.layout.additional_subtype_dialog);
+ setPersistent(false);
+ mProxy = proxy;
+ setSubtype(subtype);
+ }
+
+ public void show() {
+ showDialog(null);
+ }
+
+ public final boolean isIncomplete() {
+ return mSubtype == null;
+ }
+
+ public InputMethodSubtype getSubtype() {
+ return mSubtype;
+ }
+
+ public void setSubtype(final InputMethodSubtype subtype) {
+ mPreviousSubtype = mSubtype;
+ mSubtype = subtype;
+ if (isIncomplete()) {
+ setTitle(null);
+ setDialogTitle(R.string.add_style);
+ setKey(KEY_NEW_SUBTYPE);
+ } else {
+ final String displayName =
+ SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype);
+ setTitle(displayName);
+ setDialogTitle(displayName);
+ setKey(KEY_PREFIX + subtype.getLocale() + "_"
+ + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype));
+ }
+ }
+
+ public void revert() {
+ setSubtype(mPreviousSubtype);
+ }
+
+ public boolean hasBeenModified() {
+ return mSubtype != null && !mSubtype.equals(mPreviousSubtype);
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+ final View v = super.onCreateDialogView();
+ mSubtypeLocaleSpinner = (Spinner) v.findViewById(R.id.subtype_locale_spinner);
+ mSubtypeLocaleSpinner.setAdapter(mProxy.getSubtypeLocaleAdapter());
+ mKeyboardLayoutSetSpinner = (Spinner) v.findViewById(R.id.keyboard_layout_set_spinner);
+ mKeyboardLayoutSetSpinner.setAdapter(mProxy.getKeyboardLayoutSetAdapter());
+ // All keyboard layout names are in the Latin script and thus left to right. That means
+ // the view would align them to the left even if the system locale is RTL, but that
+ // would look strange. To fix this, we align them to the view's start, which will be
+ // natural for any direction.
+ ViewCompatUtils.setTextAlignment(
+ mKeyboardLayoutSetSpinner, ViewCompatUtils.TEXT_ALIGNMENT_VIEW_START);
+ return v;
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) {
+ builder.setCancelable(true).setOnCancelListener(this);
+ if (isIncomplete()) {
+ builder.setPositiveButton(R.string.add, this)
+ .setNegativeButton(android.R.string.cancel, this);
+ } else {
+ builder.setPositiveButton(R.string.save, this)
+ .setNeutralButton(android.R.string.cancel, this)
+ .setNegativeButton(R.string.remove, this);
+ final SubtypeLocaleItem localeItem = new SubtypeLocaleItem(mSubtype);
+ final KeyboardLayoutSetItem layoutItem = new KeyboardLayoutSetItem(mSubtype);
+ setSpinnerPosition(mSubtypeLocaleSpinner, localeItem);
+ setSpinnerPosition(mKeyboardLayoutSetSpinner, layoutItem);
+ }
+ }
+
+ private static void setSpinnerPosition(final Spinner spinner, final Object itemToSelect) {
+ final SpinnerAdapter adapter = spinner.getAdapter();
+ final int count = adapter.getCount();
+ for (int i = 0; i < count; i++) {
+ final Object item = spinner.getItemAtPosition(i);
+ if (item.equals(itemToSelect)) {
+ spinner.setSelection(i);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onCancel(final DialogInterface dialog) {
+ if (isIncomplete()) {
+ mProxy.onRemoveCustomInputStyle(this);
+ }
+ }
+
+ @Override
+ public void onClick(final DialogInterface dialog, final int which) {
+ super.onClick(dialog, which);
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ final boolean isEditing = !isIncomplete();
+ final SubtypeLocaleItem locale =
+ (SubtypeLocaleItem) mSubtypeLocaleSpinner.getSelectedItem();
+ final KeyboardLayoutSetItem layout =
+ (KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem();
+ final InputMethodSubtype subtype =
+ AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+ locale.mLocaleString, layout.mLayoutName);
+ setSubtype(subtype);
+ notifyChanged();
+ if (isEditing) {
+ mProxy.onSaveCustomInputStyle(this);
+ } else {
+ mProxy.onAddCustomInputStyle(this);
+ }
+ break;
+ case DialogInterface.BUTTON_NEUTRAL:
+ // Nothing to do
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ mProxy.onRemoveCustomInputStyle(this);
+ break;
+ }
+ }
+
+ private static int getSpinnerPosition(final Spinner spinner) {
+ if (spinner == null) return -1;
+ return spinner.getSelectedItemPosition();
+ }
+
+ private static void setSpinnerPosition(final Spinner spinner, final int position) {
+ if (spinner == null || position < 0) return;
+ spinner.setSelection(position);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ final Dialog dialog = getDialog();
+ if (dialog == null || !dialog.isShowing()) {
+ return superState;
+ }
+
+ final SavedState myState = new SavedState(superState);
+ myState.mSubtype = mSubtype;
+ myState.mSubtypeLocaleSelectedPos = getSpinnerPosition(mSubtypeLocaleSpinner);
+ myState.mKeyboardLayoutSetSelectedPos = getSpinnerPosition(mKeyboardLayoutSetSpinner);
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ final SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ setSpinnerPosition(mSubtypeLocaleSpinner, myState.mSubtypeLocaleSelectedPos);
+ setSpinnerPosition(mKeyboardLayoutSetSpinner, myState.mKeyboardLayoutSetSelectedPos);
+ setSubtype(myState.mSubtype);
+ }
+
+ static final class SavedState extends Preference.BaseSavedState {
+ InputMethodSubtype mSubtype;
+ int mSubtypeLocaleSelectedPos;
+ int mKeyboardLayoutSetSelectedPos;
+
+ public SavedState(final Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(final Parcel dest, final int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mSubtypeLocaleSelectedPos);
+ dest.writeInt(mKeyboardLayoutSetSelectedPos);
+ dest.writeParcelable(mSubtype, 0);
+ }
+
+ public SavedState(final Parcel source) {
+ super(source);
+ mSubtypeLocaleSelectedPos = source.readInt();
+ mKeyboardLayoutSetSelectedPos = source.readInt();
+ mSubtype = (InputMethodSubtype)source.readParcelable(null);
+ }
+
+ @SuppressWarnings("hiding")
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(final Parcel source) {
+ return new SavedState(source);
+ }
+
+ @Override
+ public SavedState[] newArray(final int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ static final class SubtypeLocaleItem implements Comparable<SubtypeLocaleItem> {
+ public final String mLocaleString;
+ private final String mDisplayName;
+
+ public SubtypeLocaleItem(final InputMethodSubtype subtype) {
+ mLocaleString = subtype.getLocale();
+ mDisplayName = SubtypeLocaleUtils.getSubtypeLocaleDisplayNameInSystemLocale(
+ mLocaleString);
+ }
+
+ // {@link ArrayAdapter<T>} that hosts the instance of this class needs {@link #toString()}
+ // to get display name.
+ @Override
+ public String toString() {
+ return mDisplayName;
+ }
+
+ @Override
+ public int compareTo(final SubtypeLocaleItem o) {
+ return mLocaleString.compareTo(o.mLocaleString);
+ }
+ }
+
+ static final class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> {
+ private static final String TAG_SUBTYPE = SubtypeLocaleAdapter.class.getSimpleName();
+
+ public SubtypeLocaleAdapter(final Context context) {
+ super(context, android.R.layout.simple_spinner_item);
+ setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+ final TreeSet<SubtypeLocaleItem> items = new TreeSet<>();
+ final InputMethodInfo imi = RichInputMethodManager.getInstance()
+ .getInputMethodInfoOfThisIme();
+ final int count = imi.getSubtypeCount();
+ for (int i = 0; i < count; i++) {
+ final InputMethodSubtype subtype = imi.getSubtypeAt(i);
+ if (DEBUG_SUBTYPE_ID) {
+ Log.d(TAG_SUBTYPE, String.format("%-6s 0x%08x %11d %s",
+ subtype.getLocale(), subtype.hashCode(), subtype.hashCode(),
+ SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype)));
+ }
+ if (InputMethodSubtypeCompatUtils.isAsciiCapable(subtype)) {
+ items.add(new SubtypeLocaleItem(subtype));
+ }
+ }
+ // TODO: Should filter out already existing combinations of locale and layout.
+ addAll(items);
+ }
+ }
+
+ static final class KeyboardLayoutSetItem {
+ public final String mLayoutName;
+ private final String mDisplayName;
+
+ public KeyboardLayoutSetItem(final InputMethodSubtype subtype) {
+ mLayoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
+ mDisplayName = SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype);
+ }
+
+ // {@link ArrayAdapter<T>} that hosts the instance of this class needs {@link #toString()}
+ // to get display name.
+ @Override
+ public String toString() {
+ return mDisplayName;
+ }
+ }
+
+ static final class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> {
+ public KeyboardLayoutSetAdapter(final Context context) {
+ super(context, android.R.layout.simple_spinner_item);
+ setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+ // TODO: Should filter out already existing combinations of locale and layout.
+ for (final String layout : SubtypeLocaleUtils.getPredefinedKeyboardLayoutSet()) {
+ // This is a dummy subtype with NO_LANGUAGE, only for display.
+ final InputMethodSubtype subtype =
+ AdditionalSubtypeUtils.createDummyAdditionalSubtype(
+ SubtypeLocaleUtils.NO_LANGUAGE, layout);
+ add(new KeyboardLayoutSetItem(subtype));
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java
index 9bc398654..46fcc7106 100644
--- a/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java
@@ -17,37 +17,27 @@
package com.android.inputmethod.latin.settings;
import android.app.AlertDialog;
-import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.preference.DialogPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceGroup;
import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
-import android.util.Pair;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
-import android.widget.ArrayAdapter;
-import android.widget.Spinner;
-import android.widget.SpinnerAdapter;
import android.widget.Toast;
-import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
-import com.android.inputmethod.compat.ViewCompatUtils;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.RichInputMethodManager;
import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
@@ -56,13 +46,18 @@ import com.android.inputmethod.latin.utils.IntentUtils;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
import java.util.ArrayList;
-import java.util.TreeSet;
-public final class CustomInputStyleSettingsFragment extends PreferenceFragment {
+public final class CustomInputStyleSettingsFragment extends PreferenceFragment
+ implements CustomInputStylePreference.Listener {
+ private static final String TAG = CustomInputStyleSettingsFragment.class.getSimpleName();
+ // Note: We would like to turn this debug flag true in order to see what input styles are
+ // defined in a bug-report.
+ private static final boolean DEBUG_CUSTOM_INPUT_STYLES = true;
+
private RichInputMethodManager mRichImm;
private SharedPreferences mPrefs;
- private SubtypeLocaleAdapter mSubtypeLocaleAdapter;
- private KeyboardLayoutSetAdapter mKeyboardLayoutSetAdapter;
+ private CustomInputStylePreference.SubtypeLocaleAdapter mSubtypeLocaleAdapter;
+ private CustomInputStylePreference.KeyboardLayoutSetAdapter mKeyboardLayoutSetAdapter;
private boolean mIsAddingNewSubtype;
private AlertDialog mSubtypeEnablerNotificationDialog;
@@ -73,326 +68,6 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment {
"is_subtype_enabler_notification_dialog_open";
private static final String KEY_SUBTYPE_FOR_SUBTYPE_ENABLER = "subtype_for_subtype_enabler";
- static final class SubtypeLocaleItem extends Pair<String, String>
- implements Comparable<SubtypeLocaleItem> {
- public SubtypeLocaleItem(final String localeString, final String displayName) {
- super(localeString, displayName);
- }
-
- public SubtypeLocaleItem(final String localeString) {
- this(localeString,
- SubtypeLocaleUtils.getSubtypeLocaleDisplayNameInSystemLocale(localeString));
- }
-
- @Override
- public String toString() {
- return second;
- }
-
- @Override
- public int compareTo(final SubtypeLocaleItem o) {
- return first.compareTo(o.first);
- }
- }
-
- static final class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> {
- private static final String TAG = SubtypeLocaleAdapter.class.getSimpleName();
- private static final boolean DEBUG_SUBTYPE_ID = false;
-
- public SubtypeLocaleAdapter(final Context context) {
- super(context, android.R.layout.simple_spinner_item);
- setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
-
- final TreeSet<SubtypeLocaleItem> items = new TreeSet<>();
- final InputMethodInfo imi = RichInputMethodManager.getInstance()
- .getInputMethodInfoOfThisIme();
- final int count = imi.getSubtypeCount();
- for (int i = 0; i < count; i++) {
- final InputMethodSubtype subtype = imi.getSubtypeAt(i);
- if (DEBUG_SUBTYPE_ID) {
- android.util.Log.d(TAG, String.format("%-6s 0x%08x %11d %s",
- subtype.getLocale(), subtype.hashCode(), subtype.hashCode(),
- SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype)));
- }
- if (InputMethodSubtypeCompatUtils.isAsciiCapable(subtype)) {
- items.add(createItem(context, subtype.getLocale()));
- }
- }
- // TODO: Should filter out already existing combinations of locale and layout.
- addAll(items);
- }
-
- public static SubtypeLocaleItem createItem(final Context context,
- final String localeString) {
- if (localeString.equals(SubtypeLocaleUtils.NO_LANGUAGE)) {
- final String displayName = context.getString(R.string.subtype_no_language);
- return new SubtypeLocaleItem(localeString, displayName);
- }
- return new SubtypeLocaleItem(localeString);
- }
- }
-
- static final class KeyboardLayoutSetItem extends Pair<String, String> {
- public KeyboardLayoutSetItem(final InputMethodSubtype subtype) {
- super(SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype),
- SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype));
- }
-
- @Override
- public String toString() {
- return second;
- }
- }
-
- static final class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> {
- public KeyboardLayoutSetAdapter(final Context context) {
- super(context, android.R.layout.simple_spinner_item);
- setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
-
- // TODO: Should filter out already existing combinations of locale and layout.
- for (final String layout : SubtypeLocaleUtils.getPredefinedKeyboardLayoutSet()) {
- // This is a dummy subtype with NO_LANGUAGE, only for display.
- final InputMethodSubtype subtype =
- AdditionalSubtypeUtils.createDummyAdditionalSubtype(
- SubtypeLocaleUtils.NO_LANGUAGE, layout);
- add(new KeyboardLayoutSetItem(subtype));
- }
- }
- }
-
- private interface SubtypeDialogProxy {
- public void onRemovePressed(SubtypePreference subtypePref);
- public void onSavePressed(SubtypePreference subtypePref);
- public void onAddPressed(SubtypePreference subtypePref);
- public SubtypeLocaleAdapter getSubtypeLocaleAdapter();
- public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter();
- }
-
- static final class SubtypePreference extends DialogPreference
- implements DialogInterface.OnCancelListener {
- private static final String KEY_PREFIX = "subtype_pref_";
- private static final String KEY_NEW_SUBTYPE = KEY_PREFIX + "new";
-
- private InputMethodSubtype mSubtype;
- private InputMethodSubtype mPreviousSubtype;
-
- private final SubtypeDialogProxy mProxy;
- private Spinner mSubtypeLocaleSpinner;
- private Spinner mKeyboardLayoutSetSpinner;
-
- public static SubtypePreference newIncompleteSubtypePreference(final Context context,
- final SubtypeDialogProxy proxy) {
- return new SubtypePreference(context, null, proxy);
- }
-
- public SubtypePreference(final Context context, final InputMethodSubtype subtype,
- final SubtypeDialogProxy proxy) {
- super(context, null);
- setDialogLayoutResource(R.layout.additional_subtype_dialog);
- setPersistent(false);
- mProxy = proxy;
- setSubtype(subtype);
- }
-
- public void show() {
- showDialog(null);
- }
-
- public final boolean isIncomplete() {
- return mSubtype == null;
- }
-
- public InputMethodSubtype getSubtype() {
- return mSubtype;
- }
-
- public void setSubtype(final InputMethodSubtype subtype) {
- mPreviousSubtype = mSubtype;
- mSubtype = subtype;
- if (isIncomplete()) {
- setTitle(null);
- setDialogTitle(R.string.add_style);
- setKey(KEY_NEW_SUBTYPE);
- } else {
- final String displayName =
- SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype);
- setTitle(displayName);
- setDialogTitle(displayName);
- setKey(KEY_PREFIX + subtype.getLocale() + "_"
- + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype));
- }
- }
-
- public void revert() {
- setSubtype(mPreviousSubtype);
- }
-
- public boolean hasBeenModified() {
- return mSubtype != null && !mSubtype.equals(mPreviousSubtype);
- }
-
- @Override
- protected View onCreateDialogView() {
- final View v = super.onCreateDialogView();
- mSubtypeLocaleSpinner = (Spinner) v.findViewById(R.id.subtype_locale_spinner);
- mSubtypeLocaleSpinner.setAdapter(mProxy.getSubtypeLocaleAdapter());
- mKeyboardLayoutSetSpinner = (Spinner) v.findViewById(R.id.keyboard_layout_set_spinner);
- mKeyboardLayoutSetSpinner.setAdapter(mProxy.getKeyboardLayoutSetAdapter());
- // All keyboard layout names are in the Latin script and thus left to right. That means
- // the view would align them to the left even if the system locale is RTL, but that
- // would look strange. To fix this, we align them to the view's start, which will be
- // natural for any direction.
- ViewCompatUtils.setTextAlignment(
- mKeyboardLayoutSetSpinner, ViewCompatUtils.TEXT_ALIGNMENT_VIEW_START);
- return v;
- }
-
- @Override
- protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) {
- final Context context = builder.getContext();
- builder.setCancelable(true).setOnCancelListener(this);
- if (isIncomplete()) {
- builder.setPositiveButton(R.string.add, this)
- .setNegativeButton(android.R.string.cancel, this);
- } else {
- builder.setPositiveButton(R.string.save, this)
- .setNeutralButton(android.R.string.cancel, this)
- .setNegativeButton(R.string.remove, this);
- final SubtypeLocaleItem localeItem = SubtypeLocaleAdapter.createItem(
- context, mSubtype.getLocale());
- final KeyboardLayoutSetItem layoutItem = new KeyboardLayoutSetItem(mSubtype);
- setSpinnerPosition(mSubtypeLocaleSpinner, localeItem);
- setSpinnerPosition(mKeyboardLayoutSetSpinner, layoutItem);
- }
- }
-
- private static void setSpinnerPosition(final Spinner spinner, final Object itemToSelect) {
- final SpinnerAdapter adapter = spinner.getAdapter();
- final int count = adapter.getCount();
- for (int i = 0; i < count; i++) {
- final Object item = spinner.getItemAtPosition(i);
- if (item.equals(itemToSelect)) {
- spinner.setSelection(i);
- return;
- }
- }
- }
-
- @Override
- public void onCancel(final DialogInterface dialog) {
- if (isIncomplete()) {
- mProxy.onRemovePressed(this);
- }
- }
-
- @Override
- public void onClick(final DialogInterface dialog, final int which) {
- super.onClick(dialog, which);
- switch (which) {
- case DialogInterface.BUTTON_POSITIVE:
- final boolean isEditing = !isIncomplete();
- final SubtypeLocaleItem locale =
- (SubtypeLocaleItem) mSubtypeLocaleSpinner.getSelectedItem();
- final KeyboardLayoutSetItem layout =
- (KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem();
- final InputMethodSubtype subtype =
- AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
- locale.first, layout.first);
- setSubtype(subtype);
- notifyChanged();
- if (isEditing) {
- mProxy.onSavePressed(this);
- } else {
- mProxy.onAddPressed(this);
- }
- break;
- case DialogInterface.BUTTON_NEUTRAL:
- // Nothing to do
- break;
- case DialogInterface.BUTTON_NEGATIVE:
- mProxy.onRemovePressed(this);
- break;
- }
- }
-
- private static int getSpinnerPosition(final Spinner spinner) {
- if (spinner == null) return -1;
- return spinner.getSelectedItemPosition();
- }
-
- private static void setSpinnerPosition(final Spinner spinner, final int position) {
- if (spinner == null || position < 0) return;
- spinner.setSelection(position);
- }
-
- @Override
- protected Parcelable onSaveInstanceState() {
- final Parcelable superState = super.onSaveInstanceState();
- final Dialog dialog = getDialog();
- if (dialog == null || !dialog.isShowing()) {
- return superState;
- }
-
- final SavedState myState = new SavedState(superState);
- myState.mSubtype = mSubtype;
- myState.mSubtypeLocaleSelectedPos = getSpinnerPosition(mSubtypeLocaleSpinner);
- myState.mKeyboardLayoutSetSelectedPos = getSpinnerPosition(mKeyboardLayoutSetSpinner);
- return myState;
- }
-
- @Override
- protected void onRestoreInstanceState(final Parcelable state) {
- if (!(state instanceof SavedState)) {
- super.onRestoreInstanceState(state);
- return;
- }
-
- final SavedState myState = (SavedState) state;
- super.onRestoreInstanceState(myState.getSuperState());
- setSpinnerPosition(mSubtypeLocaleSpinner, myState.mSubtypeLocaleSelectedPos);
- setSpinnerPosition(mKeyboardLayoutSetSpinner, myState.mKeyboardLayoutSetSelectedPos);
- setSubtype(myState.mSubtype);
- }
-
- static final class SavedState extends Preference.BaseSavedState {
- InputMethodSubtype mSubtype;
- int mSubtypeLocaleSelectedPos;
- int mKeyboardLayoutSetSelectedPos;
-
- public SavedState(final Parcelable superState) {
- super(superState);
- }
-
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- super.writeToParcel(dest, flags);
- dest.writeInt(mSubtypeLocaleSelectedPos);
- dest.writeInt(mKeyboardLayoutSetSelectedPos);
- dest.writeParcelable(mSubtype, 0);
- }
-
- public SavedState(final Parcel source) {
- super(source);
- mSubtypeLocaleSelectedPos = source.readInt();
- mKeyboardLayoutSetSelectedPos = source.readInt();
- mSubtype = (InputMethodSubtype)source.readParcelable(null);
- }
-
- public static final Parcelable.Creator<SavedState> CREATOR =
- new Parcelable.Creator<SavedState>() {
- @Override
- public SavedState createFromParcel(final Parcel source) {
- return new SavedState(source);
- }
-
- @Override
- public SavedState[] newArray(final int size) {
- return new SavedState[size];
- }
- };
- }
- }
-
public CustomInputStyleSettingsFragment() {
// Empty constructor for fragment generation.
}
@@ -440,18 +115,22 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment {
@Override
public void onActivityCreated(final Bundle savedInstanceState) {
final Context context = getActivity();
- mSubtypeLocaleAdapter = new SubtypeLocaleAdapter(context);
- mKeyboardLayoutSetAdapter = new KeyboardLayoutSetAdapter(context);
+ mSubtypeLocaleAdapter = new CustomInputStylePreference.SubtypeLocaleAdapter(context);
+ mKeyboardLayoutSetAdapter =
+ new CustomInputStylePreference.KeyboardLayoutSetAdapter(context);
final String prefSubtypes =
Settings.readPrefAdditionalSubtypes(mPrefs, getResources());
+ if (DEBUG_CUSTOM_INPUT_STYLES) {
+ Log.i(TAG, "Load custom input styles: " + prefSubtypes);
+ }
setPrefSubtypes(prefSubtypes, context);
mIsAddingNewSubtype = (savedInstanceState != null)
&& savedInstanceState.containsKey(KEY_IS_ADDING_NEW_SUBTYPE);
if (mIsAddingNewSubtype) {
getPreferenceScreen().addPreference(
- SubtypePreference.newIncompleteSubtypePreference(context, mSubtypeProxy));
+ CustomInputStylePreference.newIncompleteSubtypePreference(context, this));
}
super.onActivityCreated(savedInstanceState);
@@ -460,8 +139,6 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment {
KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN)) {
mSubtypePreferenceKeyForSubtypeEnabler = savedInstanceState.getString(
KEY_SUBTYPE_FOR_SUBTYPE_ENABLER);
- final SubtypePreference subtypePref = (SubtypePreference)findPreference(
- mSubtypePreferenceKeyForSubtypeEnabler);
mSubtypeEnablerNotificationDialog = createDialog();
mSubtypeEnablerNotificationDialog.show();
}
@@ -481,62 +158,60 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment {
}
}
- private final SubtypeDialogProxy mSubtypeProxy = new SubtypeDialogProxy() {
- @Override
- public void onRemovePressed(final SubtypePreference subtypePref) {
- mIsAddingNewSubtype = false;
- final PreferenceGroup group = getPreferenceScreen();
- group.removePreference(subtypePref);
+ @Override
+ public void onRemoveCustomInputStyle(final CustomInputStylePreference stylePref) {
+ mIsAddingNewSubtype = false;
+ final PreferenceGroup group = getPreferenceScreen();
+ group.removePreference(stylePref);
+ mRichImm.setAdditionalInputMethodSubtypes(getSubtypes());
+ }
+
+ @Override
+ public void onSaveCustomInputStyle(final CustomInputStylePreference stylePref) {
+ final InputMethodSubtype subtype = stylePref.getSubtype();
+ if (!stylePref.hasBeenModified()) {
+ return;
+ }
+ if (findDuplicatedSubtype(subtype) == null) {
mRichImm.setAdditionalInputMethodSubtypes(getSubtypes());
+ return;
}
- @Override
- public void onSavePressed(final SubtypePreference subtypePref) {
- final InputMethodSubtype subtype = subtypePref.getSubtype();
- if (!subtypePref.hasBeenModified()) {
- return;
- }
- if (findDuplicatedSubtype(subtype) == null) {
- mRichImm.setAdditionalInputMethodSubtypes(getSubtypes());
- return;
- }
+ // Saved subtype is duplicated.
+ final PreferenceGroup group = getPreferenceScreen();
+ group.removePreference(stylePref);
+ stylePref.revert();
+ group.addPreference(stylePref);
+ showSubtypeAlreadyExistsToast(subtype);
+ }
- // Saved subtype is duplicated.
- final PreferenceGroup group = getPreferenceScreen();
- group.removePreference(subtypePref);
- subtypePref.revert();
- group.addPreference(subtypePref);
- showSubtypeAlreadyExistsToast(subtype);
+ @Override
+ public void onAddCustomInputStyle(final CustomInputStylePreference stylePref) {
+ mIsAddingNewSubtype = false;
+ final InputMethodSubtype subtype = stylePref.getSubtype();
+ if (findDuplicatedSubtype(subtype) == null) {
+ mRichImm.setAdditionalInputMethodSubtypes(getSubtypes());
+ mSubtypePreferenceKeyForSubtypeEnabler = stylePref.getKey();
+ mSubtypeEnablerNotificationDialog = createDialog();
+ mSubtypeEnablerNotificationDialog.show();
+ return;
}
- @Override
- public void onAddPressed(final SubtypePreference subtypePref) {
- mIsAddingNewSubtype = false;
- final InputMethodSubtype subtype = subtypePref.getSubtype();
- if (findDuplicatedSubtype(subtype) == null) {
- mRichImm.setAdditionalInputMethodSubtypes(getSubtypes());
- mSubtypePreferenceKeyForSubtypeEnabler = subtypePref.getKey();
- mSubtypeEnablerNotificationDialog = createDialog();
- mSubtypeEnablerNotificationDialog.show();
- return;
- }
-
- // Newly added subtype is duplicated.
- final PreferenceGroup group = getPreferenceScreen();
- group.removePreference(subtypePref);
- showSubtypeAlreadyExistsToast(subtype);
- }
+ // Newly added subtype is duplicated.
+ final PreferenceGroup group = getPreferenceScreen();
+ group.removePreference(stylePref);
+ showSubtypeAlreadyExistsToast(subtype);
+ }
- @Override
- public SubtypeLocaleAdapter getSubtypeLocaleAdapter() {
- return mSubtypeLocaleAdapter;
- }
+ @Override
+ public CustomInputStylePreference.SubtypeLocaleAdapter getSubtypeLocaleAdapter() {
+ return mSubtypeLocaleAdapter;
+ }
- @Override
- public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter() {
- return mKeyboardLayoutSetAdapter;
- }
- };
+ @Override
+ public CustomInputStylePreference.KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter() {
+ return mKeyboardLayoutSetAdapter;
+ }
private void showSubtypeAlreadyExistsToast(final InputMethodSubtype subtype) {
final Context context = getActivity();
@@ -554,6 +229,7 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment {
}
private AlertDialog createDialog() {
+ final String imeId = mRichImm.getInputMethodIdOfThisIme();
final AlertDialog.Builder builder = new AlertDialog.Builder(
DialogUtils.getPlatformDialogThemeContext(getActivity()));
builder.setTitle(R.string.custom_input_styles_title)
@@ -563,7 +239,7 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment {
@Override
public void onClick(DialogInterface dialog, int which) {
final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
- mRichImm.getInputMethodIdOfThisIme(),
+ imeId,
Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -583,8 +259,8 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment {
final InputMethodSubtype[] subtypesArray =
AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtypes);
for (final InputMethodSubtype subtype : subtypesArray) {
- final SubtypePreference pref = new SubtypePreference(
- context, subtype, mSubtypeProxy);
+ final CustomInputStylePreference pref =
+ new CustomInputStylePreference(context, subtype, this);
group.addPreference(pref);
}
}
@@ -595,8 +271,8 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment {
final int count = group.getPreferenceCount();
for (int i = 0; i < count; i++) {
final Preference pref = group.getPreference(i);
- if (pref instanceof SubtypePreference) {
- final SubtypePreference subtypePref = (SubtypePreference)pref;
+ if (pref instanceof CustomInputStylePreference) {
+ final CustomInputStylePreference subtypePref = (CustomInputStylePreference)pref;
// We should not save newly adding subtype to preference because it is incomplete.
if (subtypePref.isIncomplete()) continue;
subtypes.add(subtypePref.getSubtype());
@@ -611,6 +287,9 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment {
final String oldSubtypes = Settings.readPrefAdditionalSubtypes(mPrefs, getResources());
final InputMethodSubtype[] subtypes = getSubtypes();
final String prefSubtypes = AdditionalSubtypeUtils.createPrefSubtypes(subtypes);
+ if (DEBUG_CUSTOM_INPUT_STYLES) {
+ Log.i(TAG, "Save custom input styles: " + prefSubtypes);
+ }
if (prefSubtypes.equals(oldSubtypes)) {
return;
}
@@ -627,8 +306,8 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment {
public boolean onOptionsItemSelected(final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.action_add_style) {
- final SubtypePreference newSubtype =
- SubtypePreference.newIncompleteSubtypePreference(getActivity(), mSubtypeProxy);
+ final CustomInputStylePreference newSubtype =
+ CustomInputStylePreference.newIncompleteSubtypePreference(getActivity(), this);
getPreferenceScreen().addPreference(newSubtype);
newSubtype.show();
mIsAddingNewSubtype = true;
diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java
index 48f4c758c..6fffb8e9d 100644
--- a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java
@@ -16,29 +16,36 @@
package com.android.inputmethod.latin.settings;
+/**
+ * Debug settings for the application.
+ *
+ * Note: Even though these settings are stored in the default shared preferences file,
+ * they shouldn't be restored across devices.
+ * If a new key is added here, it should also be blacklisted for restore in
+ * {@link LocalSettingsConstants}.
+ */
public final class DebugSettings {
public static final String PREF_DEBUG_MODE = "debug_mode";
public static final String PREF_FORCE_NON_DISTINCT_MULTITOUCH = "force_non_distinct_multitouch";
- public static final String PREF_FORCE_PHYSICAL_KEYBOARD_SPECIAL_KEY =
- "force_physical_keyboard_special_key";
- public static final String PREF_SHOW_UI_TO_ACCEPT_TYPED_WORD =
- "pref_show_ui_to_accept_typed_word";
public static final String PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS =
"pref_has_custom_key_preview_animation_params";
- public static final String PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE =
- "pref_key_preview_show_up_start_x_scale";
- public static final String PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE =
- "pref_key_preview_show_up_start_y_scale";
+ public static final String PREF_RESIZE_KEYBOARD = "pref_resize_keyboard";
+ public static final String PREF_KEYBOARD_HEIGHT_SCALE = "pref_keyboard_height_scale";
+ public static final String PREF_KEY_PREVIEW_DISMISS_DURATION =
+ "pref_key_preview_dismiss_duration";
public static final String PREF_KEY_PREVIEW_DISMISS_END_X_SCALE =
"pref_key_preview_dismiss_end_x_scale";
public static final String PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE =
"pref_key_preview_dismiss_end_y_scale";
public static final String PREF_KEY_PREVIEW_SHOW_UP_DURATION =
"pref_key_preview_show_up_duration";
- public static final String PREF_KEY_PREVIEW_DISMISS_DURATION =
- "pref_key_preview_dismiss_duration";
+ public static final String PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE =
+ "pref_key_preview_show_up_start_x_scale";
+ public static final String PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE =
+ "pref_key_preview_show_up_start_y_scale";
+ public static final String PREF_SHOULD_SHOW_LXX_SUGGESTION_UI =
+ "pref_should_show_lxx_suggestion_ui";
public static final String PREF_SLIDING_KEY_INPUT_PREVIEW = "pref_sliding_key_input_preview";
- public static final String PREF_KEY_LONGPRESS_TIMEOUT = "pref_key_longpress_timeout";
private DebugSettings() {
// This class is not publicly instantiable.
diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java
index 5640e2039..068f56df1 100644
--- a/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java
@@ -56,8 +56,8 @@ public final class DebugSettingsFragment extends SubScreenFragment
super.onCreate(icicle);
addPreferencesFromResource(R.xml.prefs_screen_debug);
- if (!Settings.HAS_UI_TO_ACCEPT_TYPED_WORD) {
- removePreference(DebugSettings.PREF_SHOW_UI_TO_ACCEPT_TYPED_WORD);
+ if (!Settings.SHOULD_SHOW_LXX_SUGGESTION_UI) {
+ removePreference(DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI);
}
mReadExternalDictionaryPref = findPreference(PREF_READ_EXTERNAL_DICTIONARY);
@@ -73,7 +73,6 @@ public final class DebugSettingsFragment extends SubScreenFragment
dictDumpPreferenceGroup.addPreference(pref);
}
final Resources res = getResources();
- setupKeyLongpressTimeoutSettings();
setupKeyPreviewAnimationDuration(DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION,
res.getInteger(R.integer.config_key_preview_show_up_duration));
setupKeyPreviewAnimationDuration(DebugSettings.PREF_KEY_PREVIEW_DISMISS_DURATION,
@@ -90,6 +89,8 @@ public final class DebugSettingsFragment extends SubScreenFragment
defaultKeyPreviewDismissEndScale);
setupKeyPreviewAnimationScale(DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE,
defaultKeyPreviewDismissEndScale);
+ setupKeyboardHeight(
+ DebugSettings.PREF_KEYBOARD_HEIGHT_SCALE, SettingsValues.DEFAULT_SIZE_SCALE);
mServiceNeedsRestart = false;
mDebugMode = (TwoStatePreference) findPreference(DebugSettings.PREF_DEBUG_MODE);
@@ -143,8 +144,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
mServiceNeedsRestart = true;
return;
}
- if (key.equals(DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH)
- || key.equals(DebugSettings.PREF_FORCE_PHYSICAL_KEYBOARD_SPECIAL_KEY)) {
+ if (key.equals(DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH)) {
mServiceNeedsRestart = true;
return;
}
@@ -163,18 +163,27 @@ public final class DebugSettingsFragment extends SubScreenFragment
}
}
- private void setupKeyLongpressTimeoutSettings() {
+ private void setupKeyPreviewAnimationScale(final String prefKey, final float defaultValue) {
final SharedPreferences prefs = getSharedPreferences();
final Resources res = getResources();
- final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(
- DebugSettings.PREF_KEY_LONGPRESS_TIMEOUT);
+ final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey);
if (pref == null) {
return;
}
pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
+ private static final float PERCENTAGE_FLOAT = 100.0f;
+
+ private float getValueFromPercentage(final int percentage) {
+ return percentage / PERCENTAGE_FLOAT;
+ }
+
+ private int getPercentageFromValue(final float floatValue) {
+ return (int)(floatValue * PERCENTAGE_FLOAT);
+ }
+
@Override
public void writeValue(final int value, final String key) {
- prefs.edit().putInt(key, value).apply();
+ prefs.edit().putFloat(key, getValueFromPercentage(value)).apply();
}
@Override
@@ -184,17 +193,21 @@ public final class DebugSettingsFragment extends SubScreenFragment
@Override
public int readValue(final String key) {
- return Settings.readKeyLongpressTimeout(prefs, res);
+ return getPercentageFromValue(
+ Settings.readKeyPreviewAnimationScale(prefs, key, defaultValue));
}
@Override
public int readDefaultValue(final String key) {
- return Settings.readDefaultKeyLongpressTimeout(res);
+ return getPercentageFromValue(defaultValue);
}
@Override
public String getValueText(final int value) {
- return res.getString(R.string.abbreviation_unit_milliseconds, value);
+ if (value < 0) {
+ return res.getString(R.string.settings_system_default);
+ }
+ return String.format(Locale.ROOT, "%d%%", value);
}
@Override
@@ -202,7 +215,7 @@ public final class DebugSettingsFragment extends SubScreenFragment
});
}
- private void setupKeyPreviewAnimationScale(final String prefKey, final float defaultValue) {
+ private void setupKeyPreviewAnimationDuration(final String prefKey, final int defaultValue) {
final SharedPreferences prefs = getSharedPreferences();
final Resources res = getResources();
final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey);
@@ -210,19 +223,9 @@ public final class DebugSettingsFragment extends SubScreenFragment
return;
}
pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
- private static final float PERCENTAGE_FLOAT = 100.0f;
-
- private float getValueFromPercentage(final int percentage) {
- return percentage / PERCENTAGE_FLOAT;
- }
-
- private int getPercentageFromValue(final float floatValue) {
- return (int)(floatValue * PERCENTAGE_FLOAT);
- }
-
@Override
public void writeValue(final int value, final String key) {
- prefs.edit().putFloat(key, getValueFromPercentage(value)).apply();
+ prefs.edit().putInt(key, value).apply();
}
@Override
@@ -232,21 +235,17 @@ public final class DebugSettingsFragment extends SubScreenFragment
@Override
public int readValue(final String key) {
- return getPercentageFromValue(
- Settings.readKeyPreviewAnimationScale(prefs, key, defaultValue));
+ return Settings.readKeyPreviewAnimationDuration(prefs, key, defaultValue);
}
@Override
public int readDefaultValue(final String key) {
- return getPercentageFromValue(defaultValue);
+ return defaultValue;
}
@Override
public String getValueText(final int value) {
- if (value < 0) {
- return res.getString(R.string.settings_system_default);
- }
- return String.format(Locale.ROOT, "%d%%", value);
+ return res.getString(R.string.abbreviation_unit_milliseconds, value);
}
@Override
@@ -254,17 +253,25 @@ public final class DebugSettingsFragment extends SubScreenFragment
});
}
- private void setupKeyPreviewAnimationDuration(final String prefKey, final int defaultValue) {
+ private void setupKeyboardHeight(final String prefKey, final float defaultValue) {
final SharedPreferences prefs = getSharedPreferences();
- final Resources res = getResources();
final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey);
if (pref == null) {
return;
}
pref.setInterface(new SeekBarDialogPreference.ValueProxy() {
+ private static final float PERCENTAGE_FLOAT = 100.0f;
+ private float getValueFromPercentage(final int percentage) {
+ return percentage / PERCENTAGE_FLOAT;
+ }
+
+ private int getPercentageFromValue(final float floatValue) {
+ return (int)(floatValue * PERCENTAGE_FLOAT);
+ }
+
@Override
public void writeValue(final int value, final String key) {
- prefs.edit().putInt(key, value).apply();
+ prefs.edit().putFloat(key, getValueFromPercentage(value)).apply();
}
@Override
@@ -274,17 +281,18 @@ public final class DebugSettingsFragment extends SubScreenFragment
@Override
public int readValue(final String key) {
- return Settings.readKeyPreviewAnimationDuration(prefs, key, defaultValue);
+ return getPercentageFromValue(
+ Settings.readKeyboardHeight(prefs, key, defaultValue));
}
@Override
public int readDefaultValue(final String key) {
- return defaultValue;
+ return getPercentageFromValue(defaultValue);
}
@Override
public String getValueText(final int value) {
- return res.getString(R.string.abbreviation_unit_milliseconds, value);
+ return String.format(Locale.ROOT, "%d%%", value);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/settings/GestureSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/GestureSettingsFragment.java
index 832fbf65a..22b0655b4 100644
--- a/java/src/com/android/inputmethod/latin/settings/GestureSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/GestureSettingsFragment.java
@@ -16,7 +16,6 @@
package com.android.inputmethod.latin.settings;
-import android.content.SharedPreferences;
import android.os.Bundle;
import com.android.inputmethod.latin.R;
diff --git a/java/src/com/android/inputmethod/latin/settings/LocalSettingsConstants.java b/java/src/com/android/inputmethod/latin/settings/LocalSettingsConstants.java
new file mode 100644
index 000000000..5c416ab18
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/settings/LocalSettingsConstants.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2014 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.settings;
+
+/**
+ * Collection of device specific preference constants.
+ */
+public class LocalSettingsConstants {
+ // Preference file for storing preferences that are tied to a device
+ // and are not backed up.
+ public static final String PREFS_FILE = "local_prefs";
+
+ // Preference key for the current account.
+ // Do not restore.
+ public static final String PREF_ACCOUNT_NAME = "pref_account_name";
+ // Preference key for enabling cloud sync feature.
+ // Do not restore.
+ public static final String PREF_ENABLE_CLOUD_SYNC = "pref_enable_cloud_sync";
+
+ // List of preference keys to skip from being restored by backup agent.
+ // These preferences are tied to a device and hence should not be restored.
+ // e.g. account name.
+ // Ideally they could have been kept in a separate file that wasn't backed up
+ // however the preference UI currently only deals with the default
+ // shared preferences which makes it non-trivial to move these out to
+ // a different shared preferences file.
+ public static final String[] PREFS_TO_SKIP_RESTORING = new String[] {
+ PREF_ACCOUNT_NAME,
+ PREF_ENABLE_CLOUD_SYNC,
+ // The debug settings are not restored on a new device.
+ // If a feature relies on these, it should ensure that the defaults are
+ // correctly set for it to work on a new device.
+ DebugSettings.PREF_DEBUG_MODE,
+ DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH,
+ DebugSettings.PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS,
+ DebugSettings.PREF_KEYBOARD_HEIGHT_SCALE,
+ DebugSettings.PREF_KEY_PREVIEW_DISMISS_DURATION,
+ DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_X_SCALE,
+ DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE,
+ DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION,
+ DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE,
+ DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE,
+ DebugSettings.PREF_RESIZE_KEYBOARD,
+ DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI,
+ DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW
+ };
+}
diff --git a/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java
index b073c50a4..c5930db1e 100644
--- a/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java
@@ -20,8 +20,6 @@ import android.os.Bundle;
import com.android.inputmethod.latin.R;
-import java.util.ArrayList;
-
/**
* "Multilingual options" settings sub screen.
*
@@ -38,5 +36,6 @@ public final class MultiLingualSettingsFragment extends SubScreenFragment {
removePreference(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY);
removePreference(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST);
}
+ AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(getActivity(), this);
}
}
diff --git a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
index 31a20c4db..7603dbba5 100644
--- a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
+++ b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
@@ -22,7 +22,8 @@ public class NativeSuggestOptions {
private static final int USE_FULL_EDIT_DISTANCE = 1;
private static final int BLOCK_OFFENSIVE_WORDS = 2;
private static final int SPACE_AWARE_GESTURE_ENABLED = 3;
- private static final int OPTIONS_SIZE = 4;
+ private static final int WEIGHT_FOR_LOCALE_IN_THOUSANDS = 4;
+ private static final int OPTIONS_SIZE = 5;
private final int[] mOptions = new int[OPTIONS_SIZE
+ AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE];
@@ -43,6 +44,12 @@ public class NativeSuggestOptions {
setBooleanOption(SPACE_AWARE_GESTURE_ENABLED, value);
}
+ public void setWeightForLocale(final float value) {
+ // We're passing this option as a fixed point value, in thousands. This is decoded in
+ // native code by SuggestOptions#weightForLocale().
+ setIntegerOption(WEIGHT_FOR_LOCALE_IN_THOUSANDS, (int) (value * 1000));
+ }
+
public void setAdditionalFeaturesOptions(final int[] additionalOptions) {
if (additionalOptions == null) {
return;
diff --git a/java/src/com/android/inputmethod/latin/settings/RadioButtonPreference.java b/java/src/com/android/inputmethod/latin/settings/RadioButtonPreference.java
index c173d4706..91444604d 100644
--- a/java/src/com/android/inputmethod/latin/settings/RadioButtonPreference.java
+++ b/java/src/com/android/inputmethod/latin/settings/RadioButtonPreference.java
@@ -43,9 +43,7 @@ public class RadioButtonPreference extends Preference {
private final View.OnClickListener mClickListener = new View.OnClickListener() {
@Override
public void onClick(final View v) {
- if (mListener != null) {
- mListener.onRadioButtonClicked(RadioButtonPreference.this);
- }
+ callListenerOnRadioButtonClicked();
}
};
@@ -67,6 +65,12 @@ public class RadioButtonPreference extends Preference {
mListener = listener;
}
+ void callListenerOnRadioButtonClicked() {
+ if (mListener != null) {
+ mListener.onRadioButtonClicked(this);
+ }
+ }
+
@Override
protected void onBindView(final View view) {
super.onBindView(view);
diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java
index 0de2d8831..16c053474 100644
--- a/java/src/com/android/inputmethod/latin/settings/Settings.java
+++ b/java/src/com/android/inputmethod/latin/settings/Settings.java
@@ -29,20 +29,24 @@ import com.android.inputmethod.compat.BuildCompatUtils;
import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
import com.android.inputmethod.latin.InputAttributes;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
import com.android.inputmethod.latin.utils.ResourceUtils;
import com.android.inputmethod.latin.utils.RunInLocale;
-import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.utils.StatsUtils;
import java.util.Collections;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
+import javax.annotation.Nonnull;
+
public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = Settings.class.getSimpleName();
// Settings screens
public static final String SCREEN_PREFERENCES = "screen_preferences";
+ public static final String SCREEN_ACCOUNTS = "screen_accounts";
public static final String SCREEN_APPEARANCE = "screen_appearance";
public static final String SCREEN_THEME = "screen_theme";
public static final String SCREEN_MULTILINGUAL = "screen_multilingual";
@@ -75,14 +79,15 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.KITKAT;
public static final boolean ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS =
BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.KITKAT;
- public static final boolean HAS_UI_TO_ACCEPT_TYPED_WORD =
- BuildCompatUtils.EFFECTIVE_SDK_INT >= BuildCompatUtils.VERSION_CODES_LXX;
+ public static final boolean SHOULD_SHOW_LXX_SUGGESTION_UI =
+ BuildCompatUtils.EFFECTIVE_SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
public static final String PREF_SHOW_LANGUAGE_SWITCH_KEY =
"pref_show_language_switch_key";
public static final String PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST =
"pref_include_other_imes_in_language_switch_list";
public static final String PREF_KEYBOARD_THEME = "pref_keyboard_theme";
public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles";
+ public static final String PREF_ENABLE_SPLIT_KEYBOARD = "pref_split_keyboard";
// TODO: consolidate key preview dismiss delay with the key preview animation parameters.
public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY =
"pref_key_preview_popup_dismiss_delay";
@@ -90,8 +95,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
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_KEYPRESS_SOUND_VOLUME = "pref_keypress_sound_volume";
+ public static final String PREF_KEY_LONGPRESS_TIMEOUT = "pref_key_longpress_timeout";
+ public static final String PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY =
+ "pref_enable_emoji_alt_physical_key";
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";
@@ -103,7 +110,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_KEY_IS_INTERNAL = "pref_key_is_internal";
public static final String PREF_ENABLE_METRICS_LOGGING = "pref_enable_metrics_logging";
-
// This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead.
// This is being used only for the backward compatibility.
private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
@@ -166,13 +172,14 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return;
}
loadSettings(mContext, mSettingsValues.mLocale, mSettingsValues.mInputAttributes);
+ StatsUtils.onLoadSettings(mSettingsValues);
} finally {
mSettingsValuesLock.unlock();
}
}
public void loadSettings(final Context context, final Locale locale,
- final InputAttributes inputAttributes) {
+ @Nonnull final InputAttributes inputAttributes) {
mSettingsValuesLock.lock();
mContext = context;
try {
@@ -206,6 +213,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return mSettingsValues.mBlockPotentiallyOffensive;
}
+ public static int readScreenMetrics(final Resources res) {
+ return res.getInteger(R.integer.config_screen_metrics);
+ }
+
// Accessed from the settings interface, hence public
public static boolean readKeypressSoundEnabled(final SharedPreferences prefs,
final Resources res) {
@@ -314,7 +325,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static int readKeyLongpressTimeout(final SharedPreferences prefs,
final Resources res) {
final int milliseconds = prefs.getInt(
- DebugSettings.PREF_KEY_LONGPRESS_TIMEOUT, UNDEFINED_PREFERENCE_VALUE_INT);
+ PREF_KEY_LONGPRESS_TIMEOUT, UNDEFINED_PREFERENCE_VALUE_INT);
return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds
: readDefaultKeyLongpressTimeout(res);
}
@@ -352,6 +363,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds : defaultValue;
}
+ public static float readKeyboardHeight(final SharedPreferences prefs,
+ final String prefKey, final float defaultValue) {
+ final float percentage = prefs.getFloat(prefKey, UNDEFINED_PREFERENCE_VALUE_FLOAT);
+ return (percentage != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? percentage : defaultValue;
+ }
+
public static boolean readUseFullscreenMode(final Resources res) {
return res.getBoolean(R.bool.config_use_fullscreen_mode);
}
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
index 4fc17387f..6c21accf6 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
@@ -25,6 +25,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.define.ProductionFlags;
import com.android.inputmethod.latin.utils.ApplicationUtils;
import com.android.inputmethod.latin.utils.FeedbackUtils;
import com.android.inputmethodcommon.InputMethodSettingsFragment;
@@ -51,6 +52,11 @@ public final class SettingsFragment extends InputMethodSettingsFragment {
final Preference multilingualOptions = findPreference(Settings.SCREEN_MULTILINGUAL);
preferenceScreen.removePreference(multilingualOptions);
}
+ if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) {
+ final Preference accountsPreference = findPreference(Settings.SCREEN_ACCOUNTS);
+ preferenceScreen.removePreference(accountsPreference);
+ }
+ AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(getActivity(), this);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index d8c548d8b..509b41fd3 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -36,17 +36,21 @@ import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
import java.util.Arrays;
import java.util.Locale;
+import javax.annotation.Nonnull;
+
/**
* When you call the constructor of this class, you may want to change the current system locale by
* using {@link com.android.inputmethod.latin.utils.RunInLocale}.
*/
-public final class SettingsValues {
+// Non-final for testing via mock library.
+public class SettingsValues {
private static final String TAG = SettingsValues.class.getSimpleName();
// "floatMaxValue" and "floatNegativeInfinity" are special marker strings for
// Float.NEGATIVE_INFINITE and Float.MAX_VALUE. Currently used for auto-correction settings.
private static final String FLOAT_MAX_VALUE_MARKER_STRING = "floatMaxValue";
private static final String FLOAT_NEGATIVE_INFINITY_MARKER_STRING = "floatNegativeInfinity";
private static final int TIMEOUT_TO_GET_TARGET_PACKAGE = 5; // seconds
+ public static final float DEFAULT_SIZE_SCALE = 1.0f; // 100%
// From resources:
public final SpacingAndPunctuations mSpacingAndPunctuations;
@@ -76,10 +80,15 @@ public final class SettingsValues {
public final boolean mSlidingKeyInputPreviewEnabled;
public final boolean mPhraseGestureEnabled;
public final int mKeyLongpressTimeout;
+ public final boolean mEnableEmojiAltPhysicalKey;
public final boolean mEnableMetricsLogging;
- public final boolean mShouldShowUiToAcceptTypedWord;
+ public final boolean mShouldShowLxxSuggestionUi;
+ // Use split layout for keyboard.
+ public final boolean mIsSplitKeyboardEnabled;
+ public final int mScreenMetrics;
// From the input box
+ @Nonnull
public final InputAttributes mInputAttributes;
// Deduced settings
@@ -102,6 +111,8 @@ public final class SettingsValues {
// Debug settings
public final boolean mIsInternal;
public final boolean mHasCustomKeyPreviewAnimationParams;
+ public final boolean mHasKeyboardResize;
+ public final float mKeyboardHeightScale;
public final int mKeyPreviewShowUpDuration;
public final int mKeyPreviewDismissDuration;
public final float mKeyPreviewShowUpStartXScale;
@@ -110,7 +121,7 @@ public final class SettingsValues {
public final float mKeyPreviewDismissEndYScale;
public SettingsValues(final Context context, final SharedPreferences prefs, final Resources res,
- final InputAttributes inputAttributes) {
+ @Nonnull final InputAttributes inputAttributes) {
mLocale = res.getConfiguration().locale;
// Get the resources
mDelayInMillisecondsToUpdateOldSuggestions =
@@ -118,12 +129,7 @@ public final class SettingsValues {
mSpacingAndPunctuations = new SpacingAndPunctuations(res);
// Store the input attributes
- if (null == inputAttributes) {
- mInputAttributes = new InputAttributes(
- null, false /* isFullscreenMode */, context.getPackageName());
- } else {
- mInputAttributes = inputAttributes;
- }
+ mInputAttributes = inputAttributes;
// Get the settings preferences
mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
@@ -153,30 +159,38 @@ public final class SettingsValues {
mDoubleSpacePeriodTimeout = res.getInteger(R.integer.config_double_space_period_timeout);
mHasHardwareKeyboard = Settings.readHasHardwareKeyboard(res.getConfiguration());
mEnableMetricsLogging = prefs.getBoolean(Settings.PREF_ENABLE_METRICS_LOGGING, true);
- mShouldShowUiToAcceptTypedWord = Settings.HAS_UI_TO_ACCEPT_TYPED_WORD
- && prefs.getBoolean(DebugSettings.PREF_SHOW_UI_TO_ACCEPT_TYPED_WORD, true);
+ mIsSplitKeyboardEnabled = prefs.getBoolean(Settings.PREF_ENABLE_SPLIT_KEYBOARD, false);
+ mScreenMetrics = Settings.readScreenMetrics(res);
+
+ mShouldShowLxxSuggestionUi = Settings.SHOULD_SHOW_LXX_SUGGESTION_UI
+ && prefs.getBoolean(DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI, true);
// Compute other readable settings
mKeyLongpressTimeout = Settings.readKeyLongpressTimeout(prefs, res);
mKeypressVibrationDuration = Settings.readKeypressVibrationDuration(prefs, res);
mKeypressSoundVolume = Settings.readKeypressSoundVolume(prefs, res);
mKeyPreviewPopupDismissDelay = Settings.readKeyPreviewPopupDismissDelay(prefs, res);
+ mEnableEmojiAltPhysicalKey = prefs.getBoolean(
+ Settings.PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY, true);
mAutoCorrectionThreshold = readAutoCorrectionThreshold(res,
autoCorrectionThresholdRawValue);
mGestureInputEnabled = Settings.readGestureInputEnabled(prefs, res);
mGestureTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true);
- mGestureFloatingPreviewTextEnabled = prefs.getBoolean(
- Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true);
+ mGestureFloatingPreviewTextEnabled = !mInputAttributes.mDisableGestureFloatingPreviewText
+ && prefs.getBoolean(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true);
mPhraseGestureEnabled = Settings.readPhraseGestureEnabled(prefs, res);
mAutoCorrectionEnabledPerUserSettings = mAutoCorrectEnabled
&& !mInputAttributes.mInputTypeNoAutoCorrect;
mSuggestionsEnabledPerUserSettings = readSuggestionsEnabled(prefs);
- AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray(
+ AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray(context,
prefs, mAdditionalFeaturesSettingValues);
mTextHighlightColorForAddToDictionaryIndicator = res.getColor(
R.color.text_decorator_add_to_dictionary_indicator_text_highlight_color);
mIsInternal = Settings.isInternal(prefs);
mHasCustomKeyPreviewAnimationParams = prefs.getBoolean(
DebugSettings.PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS, false);
+ mHasKeyboardResize = prefs.getBoolean(DebugSettings.PREF_RESIZE_KEYBOARD, false);
+ mKeyboardHeightScale = Settings.readKeyboardHeight(
+ prefs, DebugSettings.PREF_KEYBOARD_HEIGHT_SCALE, DEFAULT_SIZE_SCALE);
mKeyPreviewShowUpDuration = Settings.readKeyPreviewAnimationDuration(
prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION,
res.getInteger(R.integer.config_key_preview_show_up_duration));
@@ -211,6 +225,10 @@ public final class SettingsValues {
}
}
+ public boolean isMetricsLoggingEnabled() {
+ return mEnableMetricsLogging;
+ }
+
public boolean isApplicationSpecifiedCompletionsOn() {
return mInputAttributes.mApplicationSpecifiedCompletionOn;
}
@@ -256,9 +274,8 @@ public final class SettingsValues {
final RichInputMethodManager imm = RichInputMethodManager.getInstance();
if (mIncludesOtherImesInLanguageSwitchList) {
return imm.hasMultipleEnabledIMEsOrSubtypes(false /* include aux subtypes */);
- } else {
- return imm.hasMultipleEnabledSubtypesInThisIme(false /* include aux subtypes */);
}
+ return imm.hasMultipleEnabledSubtypesInThisIme(false /* include aux subtypes */);
}
public boolean isSameInputType(final EditorInfo editorInfo) {
diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
index 49d81104d..70d97a5ba 100644
--- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
+++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
@@ -20,10 +20,10 @@ import android.content.res.Resources;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.internal.MoreKeySpec;
-import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.PunctuationSuggestions;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
import java.util.Arrays;
import java.util.Locale;
@@ -36,6 +36,8 @@ public final class SpacingAndPunctuations {
public final int[] mSortedWordSeparators;
public final PunctuationSuggestions mSuggestPuncList;
private final int mSentenceSeparator;
+ private final int mAbbreviationMarker;
+ private final int[] mSortedSentenceTerminators;
public final String mSentenceSeparatorAndSpace;
public final boolean mCurrentLanguageHasSpaces;
public final boolean mUsesAmericanTypography;
@@ -55,7 +57,10 @@ public final class SpacingAndPunctuations {
res.getString(R.string.symbols_word_connectors));
mSortedWordSeparators = StringUtils.toSortedCodePointArray(
res.getString(R.string.symbols_word_separators));
+ mSortedSentenceTerminators = StringUtils.toSortedCodePointArray(
+ res.getString(R.string.symbols_sentence_terminators));
mSentenceSeparator = res.getInteger(R.integer.sentence_separator);
+ mAbbreviationMarker = res.getInteger(R.integer.abbreviation_marker);
mSentenceSeparatorAndSpace = new String(new int[] {
mSentenceSeparator, Constants.CODE_SPACE }, 0, 2);
mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces);
@@ -77,8 +82,10 @@ public final class SpacingAndPunctuations {
mSortedSymbolsClusteringTogether = model.mSortedSymbolsClusteringTogether;
mSortedWordConnectors = model.mSortedWordConnectors;
mSortedWordSeparators = overrideSortedWordSeparators;
+ mSortedSentenceTerminators = model.mSortedSentenceTerminators;
mSuggestPuncList = model.mSuggestPuncList;
mSentenceSeparator = model.mSentenceSeparator;
+ mAbbreviationMarker = model.mAbbreviationMarker;
mSentenceSeparatorAndSpace = model.mSentenceSeparatorAndSpace;
mCurrentLanguageHasSpaces = model.mCurrentLanguageHasSpaces;
mUsesAmericanTypography = model.mUsesAmericanTypography;
@@ -109,6 +116,14 @@ public final class SpacingAndPunctuations {
return Arrays.binarySearch(mSortedSymbolsClusteringTogether, code) >= 0;
}
+ public boolean isSentenceTerminator(final int code) {
+ return Arrays.binarySearch(mSortedSentenceTerminators, code) >= 0;
+ }
+
+ public boolean isAbbreviationMarker(final int code) {
+ return code == mAbbreviationMarker;
+ }
+
public boolean isSentenceSeparator(final int code) {
return code == mSentenceSeparator;
}
diff --git a/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java b/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java
index ca5b395ce..240f8f89b 100644
--- a/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java
@@ -20,6 +20,7 @@ import android.app.backup.BackupManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.Resources;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.Preference;
@@ -79,6 +80,16 @@ abstract class SubScreenFragment extends PreferenceFragment
return getPreferenceManager().getSharedPreferences();
}
+ /**
+ * Gets the application name to display on the UI.
+ */
+ final String getApplicationName() {
+ final Context context = getActivity();
+ final Resources res = getResources();
+ final int applicationLabelRes = context.getApplicationInfo().labelRes;
+ return res.getString(applicationLabelRes);
+ }
+
@Override
public void addPreferencesFromResource(final int preferencesResId) {
super.addPreferencesFromResource(preferencesResId);
diff --git a/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java b/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java
new file mode 100644
index 000000000..254bc6567
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 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.settings;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Test activity to use when testing preference fragments. <br/>
+ * Usage: <br/>
+ * Create an ActivityInstrumentationTestCase2 for this activity
+ * and call setIntent() with an intent that specifies the fragment to load in the activity.
+ * The fragment can then be obtained from this activity and used for testing/verification.
+ */
+public final class TestFragmentActivity extends Activity {
+ /**
+ * The fragment name that should be loaded when starting this activity.
+ * This must be specified when starting this activity, as this activity is only
+ * meant to test fragments from instrumentation tests.
+ */
+ public static final String EXTRA_SHOW_FRAGMENT = "show_fragment";
+
+ public Fragment mFragment;
+
+ @Override
+ protected void onCreate(final Bundle savedState) {
+ super.onCreate(savedState);
+ final Intent intent = getIntent();
+ final String fragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
+ if (fragmentName == null) {
+ throw new IllegalArgumentException("No fragment name specified for testing");
+ }
+
+ mFragment = Fragment.instantiate(this, fragmentName);
+ FragmentManager fragmentManager = getFragmentManager();
+ fragmentManager.beginTransaction().add(mFragment, fragmentName).commit();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/settings/ThemeSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/ThemeSettingsFragment.java
index 5a3fc3600..29289aed2 100644
--- a/java/src/com/android/inputmethod/latin/settings/ThemeSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/ThemeSettingsFragment.java
@@ -17,7 +17,6 @@
package com.android.inputmethod.latin.settings;
import android.content.Context;
-import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.Preference;
@@ -32,12 +31,12 @@ import com.android.inputmethod.latin.settings.RadioButtonPreference.OnRadioButto
*/
public final class ThemeSettingsFragment extends SubScreenFragment
implements OnRadioButtonClickedListener {
- private String mSelectedThemeId;
+ private int mSelectedThemeId;
static class KeyboardThemePreference extends RadioButtonPreference {
- final String mThemeId;
+ final int mThemeId;
- KeyboardThemePreference(final Context context, final String name, final String id) {
+ KeyboardThemePreference(final Context context, final String name, final int id) {
super(context);
setTitle(name);
mThemeId = id;
@@ -45,14 +44,13 @@ public final class ThemeSettingsFragment extends SubScreenFragment
}
static void updateKeyboardThemeSummary(final Preference pref) {
- final Resources res = pref.getContext().getResources();
- final SharedPreferences prefs = pref.getSharedPreferences();
- final KeyboardTheme keyboardTheme = KeyboardTheme.getKeyboardTheme(prefs);
- final String keyboardThemeId = String.valueOf(keyboardTheme.mThemeId);
+ final Context context = pref.getContext();
+ final Resources res = context.getResources();
+ final KeyboardTheme keyboardTheme = KeyboardTheme.getKeyboardTheme(context);
final String[] keyboardThemeNames = res.getStringArray(R.array.keyboard_theme_names);
- final String[] keyboardThemeIds = res.getStringArray(R.array.keyboard_theme_ids);
+ final int[] keyboardThemeIds = res.getIntArray(R.array.keyboard_theme_ids);
for (int index = 0; index < keyboardThemeNames.length; index++) {
- if (keyboardThemeId.equals(keyboardThemeIds[index])) {
+ if (keyboardTheme.mThemeId == keyboardThemeIds[index]) {
pref.setSummary(keyboardThemeNames[index]);
return;
}
@@ -64,18 +62,18 @@ public final class ThemeSettingsFragment extends SubScreenFragment
super.onCreate(icicle);
addPreferencesFromResource(R.xml.prefs_screen_theme);
final PreferenceScreen screen = getPreferenceScreen();
+ final Context context = getActivity();
final Resources res = getResources();
final String[] keyboardThemeNames = res.getStringArray(R.array.keyboard_theme_names);
- final String[] keyboardThemeIds = res.getStringArray(R.array.keyboard_theme_ids);
+ final int[] keyboardThemeIds = res.getIntArray(R.array.keyboard_theme_ids);
for (int index = 0; index < keyboardThemeNames.length; index++) {
final KeyboardThemePreference pref = new KeyboardThemePreference(
- getActivity(), keyboardThemeNames[index], keyboardThemeIds[index]);
+ context, keyboardThemeNames[index], keyboardThemeIds[index]);
screen.addPreference(pref);
pref.setOnRadioButtonClickedListener(this);
}
- final SharedPreferences prefs = getSharedPreferences();
- final KeyboardTheme keyboardTheme = KeyboardTheme.getKeyboardTheme(prefs);
- mSelectedThemeId = String.valueOf(keyboardTheme.mThemeId);
+ final KeyboardTheme keyboardTheme = KeyboardTheme.getKeyboardTheme(context);
+ mSelectedThemeId = keyboardTheme.mThemeId;
}
@Override
@@ -106,7 +104,7 @@ public final class ThemeSettingsFragment extends SubScreenFragment
final Preference preference = screen.getPreference(index);
if (preference instanceof KeyboardThemePreference) {
final KeyboardThemePreference pref = (KeyboardThemePreference)preference;
- final boolean selected = mSelectedThemeId.equals(pref.mThemeId);
+ final boolean selected = (mSelectedThemeId == pref.mThemeId);
pref.setSelected(selected);
}
}
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
index b770ea512..7607429f8 100644
--- a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
+++ b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
@@ -17,12 +17,8 @@
package com.android.inputmethod.latin.setup;
import android.app.Activity;
-import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
-import android.provider.Settings;
-import android.view.inputmethod.InputMethodInfo;
-import android.view.inputmethod.InputMethodManager;
public final class SetupActivity extends Activity {
@Override
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
index e455e53d3..c3b30dcb4 100644
--- a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
+++ b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
@@ -42,10 +42,14 @@ import com.android.inputmethod.latin.utils.UncachedInputMethodManagerUtils;
import java.util.ArrayList;
+import javax.annotation.Nonnull;
+
// TODO: Use Fragment to implement welcome screen and setup steps.
public final class SetupWizardActivity extends Activity implements View.OnClickListener {
static final String TAG = SetupWizardActivity.class.getSimpleName();
+ // For debugging purpose.
+ private static final boolean FORCE_TO_SHOW_WELCOME_SCREEN = false;
private static final boolean ENABLE_WELCOME_VIDEO = true;
private InputMethodManager mImm;
@@ -80,7 +84,7 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
private final InputMethodManager mImmInHandler;
- public SettingsPoolingHandler(final SetupWizardActivity ownerInstance,
+ public SettingsPoolingHandler(@Nonnull final SetupWizardActivity ownerInstance,
final InputMethodManager imm) {
super(ownerInstance);
mImmInHandler = imm;
@@ -304,6 +308,9 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
private int determineSetupStepNumber() {
mHandler.cancelPollingImeSettings();
+ if (FORCE_TO_SHOW_WELCOME_SCREEN) {
+ return STEP_1;
+ }
if (!UncachedInputMethodManagerUtils.isThisImeEnabled(this, mImm)) {
return STEP_1;
}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 90398deb2..bcf7bbfdc 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -16,14 +16,11 @@
package com.android.inputmethod.latin.spellcheck;
-import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.service.textservice.SpellCheckerService;
import android.text.InputType;
-import android.util.Log;
-import android.util.LruCache;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.view.textservice.SuggestionsInfo;
@@ -32,109 +29,55 @@ import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardId;
import com.android.inputmethod.keyboard.KeyboardLayoutSet;
import com.android.inputmethod.keyboard.ProximityInfo;
-import com.android.inputmethod.latin.ContactsBinaryDictionary;
-import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.DictionaryCollection;
import com.android.inputmethod.latin.DictionaryFacilitator;
-import com.android.inputmethod.latin.DictionaryFactory;
-import com.android.inputmethod.latin.PrevWordsInfo;
+import com.android.inputmethod.latin.DictionaryFacilitatorLruCache;
+import com.android.inputmethod.latin.NgramContext;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
-import com.android.inputmethod.latin.UserBinaryDictionary;
import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
-import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
-import com.android.inputmethod.latin.utils.CollectionUtils;
-import com.android.inputmethod.latin.utils.LocaleUtils;
import com.android.inputmethod.latin.utils.ScriptUtils;
-import com.android.inputmethod.latin.utils.StringUtils;
import com.android.inputmethod.latin.utils.SuggestionResults;
import com.android.inputmethod.latin.WordComposer;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
import java.util.Locale;
-import java.util.Map;
-import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
/**
* Service for spell checking, using LatinIME's dictionaries and mechanisms.
*/
public final class AndroidSpellCheckerService extends SpellCheckerService
implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
- private static final boolean DBG = false;
-
public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480;
- private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 368;
+ private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 301;
private static final String DICTIONARY_NAME_PREFIX = "spellcheck_";
- private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000;
- private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5;
private static final String[] EMPTY_STRING_ARRAY = new String[0];
- private final HashSet<Locale> mCachedLocales = new HashSet<>();
-
private final int MAX_NUM_OF_THREADS_READ_DICTIONARY = 2;
private final Semaphore mSemaphore = new Semaphore(MAX_NUM_OF_THREADS_READ_DICTIONARY,
true /* fair */);
// TODO: Make each spell checker session has its own session id.
private final ConcurrentLinkedQueue<Integer> mSessionIdPool = new ConcurrentLinkedQueue<>();
- private static class DictionaryFacilitatorLruCache extends
- LruCache<Locale, DictionaryFacilitator> {
- private final HashSet<Locale> mCachedLocales;
- public DictionaryFacilitatorLruCache(final HashSet<Locale> cachedLocales, int maxSize) {
- super(maxSize);
- mCachedLocales = cachedLocales;
- }
-
- @Override
- protected void entryRemoved(boolean evicted, Locale key,
- DictionaryFacilitator oldValue, DictionaryFacilitator newValue) {
- if (oldValue != null && oldValue != newValue) {
- oldValue.closeDictionaries();
- }
- if (key != null && newValue == null) {
- // Remove locale from the cache when the dictionary facilitator for the locale is
- // evicted and new facilitator is not set for the locale.
- mCachedLocales.remove(key);
- if (size() >= maxSize()) {
- Log.w(TAG, "DictionaryFacilitator for " + key.toString()
- + " has been evicted due to cache size limit."
- + " size: " + size() + ", maxSize: " + maxSize());
- }
- }
- }
- }
-
private static final int MAX_DICTIONARY_FACILITATOR_COUNT = 3;
- private final LruCache<Locale, DictionaryFacilitator> mDictionaryFacilitatorCache =
- new DictionaryFacilitatorLruCache(mCachedLocales, MAX_DICTIONARY_FACILITATOR_COUNT);
+ private final DictionaryFacilitatorLruCache mDictionaryFacilitatorCache =
+ new DictionaryFacilitatorLruCache(this /* context */, MAX_DICTIONARY_FACILITATOR_COUNT,
+ DICTIONARY_NAME_PREFIX);
private final ConcurrentHashMap<Locale, Keyboard> mKeyboardCache = new ConcurrentHashMap<>();
// The threshold for a suggestion to be considered "recommended".
private float mRecommendedThreshold;
- // Whether to use the contacts dictionary
- private boolean mUseContactsDictionary;
// TODO: make a spell checker option to block offensive words or not
private final SettingsValuesForSuggestion mSettingsValuesForSuggestion =
new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */,
true /* spaceAwareGestureEnabled */,
null /* additionalFeaturesSettingValues */);
- private final Object mDictionaryLock = new Object();
public static final String SINGLE_QUOTE = "\u0027";
public static final String APOSTROPHE = "\u2019";
@@ -176,20 +119,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
if (!PREF_USE_CONTACTS_KEY.equals(key)) return;
final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true);
- if (useContactsDictionary != mUseContactsDictionary) {
- mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY);
- try {
- mUseContactsDictionary = useContactsDictionary;
- for (final Locale locale : mCachedLocales) {
- final DictionaryFacilitator dictionaryFacilitator =
- mDictionaryFacilitatorCache.get(locale);
- resetDictionariesForLocale(this /* context */,
- dictionaryFacilitator, locale, mUseContactsDictionary);
- }
- } finally {
- mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY);
- }
- }
+ mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary);
}
@Override
@@ -222,7 +152,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
mSemaphore.acquireUninterruptibly();
try {
DictionaryFacilitator dictionaryFacilitatorForLocale =
- getDictionaryFacilitatorForLocaleLocked(locale);
+ mDictionaryFacilitatorCache.get(locale);
return dictionaryFacilitatorForLocale.isValidWord(word, false /* igroreCase */);
} finally {
mSemaphore.release();
@@ -230,15 +160,16 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
}
public SuggestionResults getSuggestionResults(final Locale locale, final WordComposer composer,
- final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo) {
+ final NgramContext ngramContext, final ProximityInfo proximityInfo) {
Integer sessionId = null;
mSemaphore.acquireUninterruptibly();
try {
sessionId = mSessionIdPool.poll();
DictionaryFacilitator dictionaryFacilitatorForLocale =
- getDictionaryFacilitatorForLocaleLocked(locale);
- return dictionaryFacilitatorForLocale.getSuggestionResults(composer, prevWordsInfo,
- proximityInfo, mSettingsValuesForSuggestion, sessionId);
+ mDictionaryFacilitatorCache.get(locale);
+ return dictionaryFacilitatorForLocale.getSuggestionResults(composer, ngramContext,
+ proximityInfo.getNativeProximityInfo(), mSettingsValuesForSuggestion,
+ sessionId);
} finally {
if (sessionId != null) {
mSessionIdPool.add(sessionId);
@@ -251,56 +182,18 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
mSemaphore.acquireUninterruptibly();
try {
final DictionaryFacilitator dictionaryFacilitator =
- getDictionaryFacilitatorForLocaleLocked(locale);
- return dictionaryFacilitator.hasInitializedMainDictionary();
+ mDictionaryFacilitatorCache.get(locale);
+ return dictionaryFacilitator.hasAtLeastOneInitializedMainDictionary();
} finally {
mSemaphore.release();
}
}
- private DictionaryFacilitator getDictionaryFacilitatorForLocaleLocked(final Locale locale) {
- DictionaryFacilitator dictionaryFacilitatorForLocale =
- mDictionaryFacilitatorCache.get(locale);
- if (dictionaryFacilitatorForLocale == null) {
- dictionaryFacilitatorForLocale = new DictionaryFacilitator();
- mDictionaryFacilitatorCache.put(locale, dictionaryFacilitatorForLocale);
- mCachedLocales.add(locale);
- resetDictionariesForLocale(this /* context */, dictionaryFacilitatorForLocale,
- locale, mUseContactsDictionary);
- }
- return dictionaryFacilitatorForLocale;
- }
-
- private static void resetDictionariesForLocale(final Context context,
- final DictionaryFacilitator dictionaryFacilitator, final Locale locale,
- final boolean useContactsDictionary) {
- dictionaryFacilitator.resetDictionariesWithDictNamePrefix(context, locale,
- useContactsDictionary, false /* usePersonalizedDicts */,
- false /* forceReloadMainDictionary */, null /* listener */,
- DICTIONARY_NAME_PREFIX);
- for (int i = 0; i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT; i++) {
- try {
- dictionaryFacilitator.waitForLoadingMainDictionary(
- WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
- return;
- } catch (final InterruptedException e) {
- Log.i(TAG, "Interrupted during waiting for loading main dictionary.", e);
- if (i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT - 1) {
- Log.i(TAG, "Retry", e);
- } else {
- Log.w(TAG, "Give up retrying. Retried "
- + MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT + " times.", e);
- }
- }
- }
- }
-
@Override
public boolean onUnbind(final Intent intent) {
mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY);
try {
mDictionaryFacilitatorCache.evictAll();
- mCachedLocales.clear();
} finally {
mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY);
}
@@ -334,7 +227,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(this, editorInfo);
builder.setKeyboardGeometry(
SPELLCHECKER_DUMMY_KEYBOARD_WIDTH, SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT);
- builder.setSubtype(subtype);
+ builder.setSubtype(new RichInputMethodSubtype(subtype));
builder.setIsSpellChecker(true /* isSpellChecker */);
builder.disableTouchPositionCorrectionData();
return builder.build();
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
index 34e01197a..2c690aea7 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
@@ -16,8 +16,10 @@
package com.android.inputmethod.latin.spellcheck;
+import android.annotation.TargetApi;
import android.content.res.Resources;
import android.os.Binder;
+import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import android.view.textservice.SentenceSuggestionsInfo;
@@ -25,8 +27,8 @@ import android.view.textservice.SuggestionsInfo;
import android.view.textservice.TextInfo;
import com.android.inputmethod.compat.TextInfoCompatUtils;
-import com.android.inputmethod.latin.PrevWordsInfo;
-import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.NgramContext;
+import com.android.inputmethod.latin.utils.SpannableStringUtils;
import java.util.ArrayList;
import java.util.Locale;
@@ -42,6 +44,7 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck
mResources = service.getResources();
}
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(TextInfo ti,
SentenceSuggestionsInfo ssi) {
final CharSequence typedText = TextInfoCompatUtils.getCharSequenceOrString(ti);
@@ -62,15 +65,16 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck
final int offset = ssi.getOffsetAt(i);
final int length = ssi.getLengthAt(i);
final CharSequence subText = typedText.subSequence(offset, offset + length);
- final PrevWordsInfo prevWordsInfo =
- new PrevWordsInfo(new PrevWordsInfo.WordInfo(currentWord));
+ final NgramContext ngramContext =
+ new NgramContext(new NgramContext.WordInfo(currentWord));
currentWord = subText;
if (!subText.toString().contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
continue;
}
- final CharSequence[] splitTexts = StringUtils.split(subText,
+ // Split preserving spans.
+ final CharSequence[] splitTexts = SpannableStringUtils.split(subText,
AndroidSpellCheckerService.SINGLE_QUOTE,
- true /* preserveTrailingEmptySegments */ );
+ true /* preserveTrailingEmptySegments */);
if (splitTexts == null || splitTexts.length <= 1) {
continue;
}
@@ -80,7 +84,7 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck
if (TextUtils.isEmpty(splitText)) {
continue;
}
- if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString(), prevWordsInfo)
+ if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString(), ngramContext)
== null) {
continue;
}
@@ -149,7 +153,7 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck
* @param textInfos an array of the text metadata
* @param suggestionsLimit the maximum number of suggestions to be returned
* @return an array of {@link SentenceSuggestionsInfo} returned by
- * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)}
+ * {@link android.service.textservice.SpellCheckerService.Session#onGetSuggestions(TextInfo, int)}
*/
private SentenceSuggestionsInfo[] splitAndSuggest(TextInfo[] textInfos, int suggestionsLimit) {
if (textInfos == null || textInfos.length == 0) {
@@ -208,10 +212,10 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck
} else {
prevWord = null;
}
- final PrevWordsInfo prevWordsInfo =
- new PrevWordsInfo(new PrevWordsInfo.WordInfo(prevWord));
+ final NgramContext ngramContext =
+ new NgramContext(new NgramContext.WordInfo(prevWord));
final TextInfo textInfo = textInfos[i];
- retval[i] = onGetSuggestionsInternal(textInfo, prevWordsInfo, suggestionsLimit);
+ retval[i] = onGetSuggestionsInternal(textInfo, ngramContext, suggestionsLimit);
retval[i].setCookieAndSequence(textInfo.getCookie(), textInfo.getSequence());
}
return retval;
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index d668672aa..3ad8fb910 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -30,15 +30,15 @@ import android.view.textservice.TextInfo;
import com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.ProximityInfo;
-import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.PrevWordsInfo;
+import com.android.inputmethod.latin.NgramContext;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
import com.android.inputmethod.latin.utils.CoordinateUtils;
import com.android.inputmethod.latin.utils.LocaleUtils;
import com.android.inputmethod.latin.utils.ScriptUtils;
-import com.android.inputmethod.latin.utils.StringUtils;
import com.android.inputmethod.latin.utils.SuggestionResults;
import java.util.ArrayList;
@@ -73,27 +73,25 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
new LruCache<>(MAX_CACHE_SIZE);
- // TODO: Support n-gram input
- private static String generateKey(final String query, final PrevWordsInfo prevWordsInfo) {
- if (TextUtils.isEmpty(query) || !prevWordsInfo.isValid()) {
+ private static String generateKey(final String query, final NgramContext ngramContext) {
+ if (TextUtils.isEmpty(query) || !ngramContext.isValid()) {
return query;
}
- return query + CHAR_DELIMITER + prevWordsInfo;
+ return query + CHAR_DELIMITER + ngramContext;
}
public SuggestionsParams getSuggestionsFromCache(String query,
- final PrevWordsInfo prevWordsInfo) {
- return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWordsInfo));
+ final NgramContext ngramContext) {
+ return mUnigramSuggestionsInfoCache.get(generateKey(query, ngramContext));
}
- public void putSuggestionsToCache(
- final String query, final PrevWordsInfo prevWordsInfo,
+ public void putSuggestionsToCache(final String query, final NgramContext ngramContext,
final String[] suggestions, final int flags) {
if (suggestions == null || TextUtils.isEmpty(query)) {
return;
}
mUnigramSuggestionsInfoCache.put(
- generateKey(query, prevWordsInfo), new SuggestionsParams(suggestions, flags));
+ generateKey(query, ngramContext), new SuggestionsParams(suggestions, flags));
}
public void clearCache() {
@@ -223,12 +221,11 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
}
protected SuggestionsInfo onGetSuggestionsInternal(
- final TextInfo textInfo, final PrevWordsInfo prevWordsInfo,
- final int suggestionsLimit) {
+ final TextInfo textInfo, final NgramContext ngramContext, final int suggestionsLimit) {
try {
final String inText = textInfo.getText();
final SuggestionsParams cachedSuggestionsParams =
- mSuggestionsCache.getSuggestionsFromCache(inText, prevWordsInfo);
+ mSuggestionsCache.getSuggestionsFromCache(inText, ngramContext);
if (cachedSuggestionsParams != null) {
if (DBG) {
Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags);
@@ -283,7 +280,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
composer.setComposingWord(codePoints, coordinates);
// TODO: Don't gather suggestions if the limit is <= 0 unless necessary
final SuggestionResults suggestionResults = mService.getSuggestionResults(
- mLocale, composer, prevWordsInfo, proximityInfo);
+ mLocale, composer, ngramContext, proximityInfo);
final Result result = getResult(capitalizeType, mLocale, suggestionsLimit,
mService.getRecommendedThreshold(), text, suggestionResults);
isInDict = isInDictForAnyCapitalization(text, capitalizeType);
@@ -308,18 +305,17 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
.getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
: 0);
final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
- mSuggestionsCache.putSuggestionsToCache(text, prevWordsInfo, result.mSuggestions,
+ mSuggestionsCache.putSuggestionsToCache(text, ngramContext, 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(
- false /* reportAsTypo */);
}
+ Log.e(TAG, "Exception while spellcheking", e);
+ return AndroidSpellCheckerService.getNotInDictEmptySuggestions(
+ false /* reportAsTypo */);
}
}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java b/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java
index 51c4b1ee8..10c458c7d 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java
@@ -16,13 +16,15 @@
package com.android.inputmethod.latin.spellcheck;
+import android.annotation.TargetApi;
import android.content.res.Resources;
+import android.os.Build;
import android.view.textservice.SentenceSuggestionsInfo;
import android.view.textservice.SuggestionsInfo;
import android.view.textservice.TextInfo;
import com.android.inputmethod.compat.TextInfoCompatUtils;
-import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
import com.android.inputmethod.latin.utils.RunInLocale;
@@ -76,19 +78,19 @@ public class SentenceLevelAdapter {
private static class WordIterator {
private final SpacingAndPunctuations mSpacingAndPunctuations;
public WordIterator(final Resources res, final Locale locale) {
- final RunInLocale<SpacingAndPunctuations> job
- = new RunInLocale<SpacingAndPunctuations>() {
+ final RunInLocale<SpacingAndPunctuations> job =
+ new RunInLocale<SpacingAndPunctuations>() {
@Override
- protected SpacingAndPunctuations job(final Resources res) {
- return new SpacingAndPunctuations(res);
+ protected SpacingAndPunctuations job(final Resources r) {
+ return new SpacingAndPunctuations(r);
}
};
mSpacingAndPunctuations = job.runInLocale(res, locale);
}
- public int getEndOfWord(final CharSequence sequence, int index) {
+ public int getEndOfWord(final CharSequence sequence, final int fromIndex) {
final int length = sequence.length();
- index = index < 0 ? 0 : Character.offsetByCodePoints(sequence, index, 1);
+ int index = fromIndex < 0 ? 0 : Character.offsetByCodePoints(sequence, fromIndex, 1);
while (index < length) {
final int codePoint = Character.codePointAt(sequence, index);
if (mSpacingAndPunctuations.isWordSeparator(codePoint)) {
@@ -111,12 +113,12 @@ public class SentenceLevelAdapter {
return index;
}
- public int getBeginningOfNextWord(final CharSequence sequence, int index) {
+ public int getBeginningOfNextWord(final CharSequence sequence, final int fromIndex) {
final int length = sequence.length();
- if (index >= length) {
+ if (fromIndex >= length) {
return -1;
}
- index = index < 0 ? 0 : Character.offsetByCodePoints(sequence, index, 1);
+ int index = fromIndex < 0 ? 0 : Character.offsetByCodePoints(sequence, fromIndex, 1);
while (index < length) {
final int codePoint = Character.codePointAt(sequence, index);
if (!mSpacingAndPunctuations.isWordSeparator(codePoint)) {
@@ -140,14 +142,13 @@ public class SentenceLevelAdapter {
final int cookie = originalTextInfo.getCookie();
final int start = -1;
final int end = originalText.length();
- final ArrayList<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>();
+ final ArrayList<SentenceWordItem> wordItems = new ArrayList<>();
int wordStart = wordIterator.getBeginningOfNextWord(originalText, start);
int wordEnd = wordIterator.getEndOfWord(originalText, wordStart);
while (wordStart <= end && wordEnd != -1 && wordStart != -1) {
if (wordEnd >= start && wordEnd > wordStart) {
- CharSequence subSequence = originalText.subSequence(wordStart, wordEnd).toString();
- final TextInfo ti = TextInfoCompatUtils.newInstance(subSequence, 0,
- subSequence.length(), cookie, subSequence.hashCode());
+ final TextInfo ti = TextInfoCompatUtils.newInstance(originalText, wordStart,
+ wordEnd, cookie, originalText.subSequence(wordStart, wordEnd).hashCode());
wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd));
}
wordStart = wordIterator.getBeginningOfNextWord(originalText, wordEnd);
@@ -159,6 +160,7 @@ public class SentenceLevelAdapter {
return new SentenceTextInfoParams(originalTextInfo, wordItems);
}
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public static SentenceSuggestionsInfo reconstructSuggestions(
SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) {
if (results == null || results.length == 0) {
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
index df9a76119..294666b8b 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
@@ -18,7 +18,9 @@ package com.android.inputmethod.latin.spellcheck;
import com.android.inputmethod.latin.utils.FragmentUtils;
+import android.annotation.TargetApi;
import android.content.Intent;
+import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceActivity;
@@ -41,8 +43,8 @@ public final class SpellCheckerSettingsActivity extends PreferenceActivity {
return modIntent;
}
- // TODO: Uncomment the override annotation once we start using SDK version 19.
- // @Override
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ @Override
public boolean isValidFragment(String fragmentName) {
return FragmentUtils.isValidFragment(fragmentName);
}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index 9d186d44d..37ab2669b 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -26,9 +26,9 @@ import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
import com.android.inputmethod.keyboard.internal.KeyboardParams;
-import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.utils.TypefaceUtils;
public final class MoreSuggestions extends Keyboard {
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
index f7b6f919d..907e3fa42 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
@@ -40,6 +40,8 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView {
public abstract void onSuggestionSelected(final SuggestedWordInfo info);
}
+ private boolean mIsInModalMode;
+
public MoreSuggestionsView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.moreKeysKeyboardViewStyle);
}
@@ -53,6 +55,7 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView {
@Override
public void setKeyboard(final Keyboard keyboard) {
super.setKeyboard(keyboard);
+ mIsInModalMode = false;
// With accessibility mode off, {@link #mAccessibilityDelegate} is set to null at the
// above {@link MoreKeysKeyboardView#setKeyboard(Keyboard)} call.
// With accessibility mode on, {@link #mAccessibilityDelegate} is set to a
@@ -74,12 +77,17 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView {
updateKeyDrawParams(keyHeight);
}
- public void adjustVerticalCorrectionForModalMode() {
+ public void setModalMode() {
+ mIsInModalMode = true;
// Set vertical correction to zero (Reset more keys keyboard sliding allowance
// {@link R#dimen.config_more_keys_keyboard_slide_allowance}).
mKeyDetector.setKeyboard(getKeyboard(), -getPaddingLeft(), -getPaddingTop());
}
+ public boolean isInModalMode() {
+ return mIsInModalMode;
+ }
+
@Override
protected void onKeyInput(final Key key, final int x, final int y) {
if (!(key instanceof MoreSuggestionKey)) {
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index 1e8df8986..27a0f62ff 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -50,16 +50,17 @@ import com.android.inputmethod.latin.PunctuationSuggestions;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.define.DebugFlags;
import com.android.inputmethod.latin.settings.Settings;
import com.android.inputmethod.latin.settings.SettingsValues;
-import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
import com.android.inputmethod.latin.utils.ResourceUtils;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
import com.android.inputmethod.latin.utils.ViewLayoutUtils;
import java.util.ArrayList;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
final class SuggestionStripLayoutHelper {
private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f;
@@ -213,15 +214,14 @@ final class SuggestionStripLayoutHelper {
return word;
}
- final int len = word.length();
final Spannable spannedWord = new SpannableString(word);
final int options = mSuggestionStripOptions;
if ((isAutoCorrection && (options & AUTO_CORRECT_BOLD) != 0)
|| (isTypedWordValid && (options & VALID_TYPED_WORD_BOLD) != 0)) {
- spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ addStyleSpan(spannedWord, BOLD_SPAN);
}
if (isAutoCorrection && (options & AUTO_CORRECT_UNDERLINE) != 0) {
- spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ addStyleSpan(spannedWord, UNDERLINE_SPAN);
}
return spannedWord;
}
@@ -238,9 +238,9 @@ final class SuggestionStripLayoutHelper {
final SettingsValues settingsValues = Settings.getInstance().getCurrent();
final boolean shouldOmitTypedWord = shouldOmitTypedWord(suggestedWords.mInputStyle,
settingsValues.mGestureFloatingPreviewTextEnabled,
- settingsValues.mShouldShowUiToAcceptTypedWord);
+ settingsValues.mShouldShowLxxSuggestionUi);
return getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords.mWillAutoCorrect,
- settingsValues.mShouldShowUiToAcceptTypedWord && shouldOmitTypedWord,
+ settingsValues.mShouldShowLxxSuggestionUi && shouldOmitTypedWord,
mCenterPositionInStrip, mTypedWordPositionWhenAutocorrect);
}
@@ -319,18 +319,6 @@ final class SuggestionStripLayoutHelper {
} else {
color = mColorSuggested;
}
- if (DebugFlags.DEBUG_ENABLED && suggestedWords.size() > 1) {
- // If we auto-correct, then the autocorrection is in slot 0 and the typed word
- // is in slot 1.
- if (indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION
- && suggestedWords.mWillAutoCorrect
- && AutoCorrectionUtils.shouldBlockAutoCorrectionBySafetyNet(
- suggestedWords.getLabel(SuggestedWords.INDEX_OF_AUTO_CORRECTION),
- suggestedWords.getLabel(SuggestedWords.INDEX_OF_TYPED_WORD))) {
- return 0xFFFF0000;
- }
- }
-
if (suggestedWords.mIsObsoleteSuggestions && !isTypedWord) {
return applyAlpha(color, mAlphaObsoleted);
}
@@ -365,17 +353,19 @@ final class SuggestionStripLayoutHelper {
(PunctuationSuggestions)suggestedWords, stripView);
}
+ final int wordCountToShow = suggestedWords.getWordCountToShow(
+ Settings.getInstance().getCurrent().mShouldShowLxxSuggestionUi);
final int startIndexOfMoreSuggestions = setupWordViewsAndReturnStartIndexOfMoreSuggestions(
suggestedWords, mSuggestionsCountInStrip);
final TextView centerWordView = mWordViews.get(mCenterPositionInStrip);
final int stripWidth = stripView.getWidth();
final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, stripWidth);
- if (suggestedWords.size() == 1 || getTextScaleX(centerWordView.getText(), centerWidth,
+ if (wordCountToShow == 1 || getTextScaleX(centerWordView.getText(), centerWidth,
centerWordView.getPaint()) < MIN_TEXT_XSCALE) {
// Layout only the most relevant suggested word at the center of the suggestion strip
// by consolidating all slots in the strip.
final int countInStrip = 1;
- mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
+ mMoreSuggestionsAvailable = (wordCountToShow > countInStrip);
layoutWord(mCenterPositionInStrip, stripWidth - mPadding);
stripView.addView(centerWordView);
setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT);
@@ -387,7 +377,8 @@ final class SuggestionStripLayoutHelper {
}
final int countInStrip = mSuggestionsCountInStrip;
- mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
+ mMoreSuggestionsAvailable = (wordCountToShow > countInStrip);
+ @SuppressWarnings("unused")
int x = 0;
for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) {
if (positionInStrip != 0) {
@@ -442,10 +433,11 @@ final class SuggestionStripLayoutHelper {
// {@link StyleSpan} in a content description may cause an issue of TTS/TalkBack.
// Use a simple {@link String} to avoid the issue.
wordView.setContentDescription(TextUtils.isEmpty(word) ? null : word.toString());
- final CharSequence text = getEllipsizedText(word, width, wordView.getPaint());
- final float scaleX = getTextScaleX(word, width, wordView.getPaint());
+ final CharSequence text = getEllipsizedTextWithSettingScaleX(
+ word, width, wordView.getPaint());
+ final float scaleX = wordView.getTextScaleX();
wordView.setText(text); // TextView.setText() resets text scale x to 1.0.
- wordView.setTextScaleX(Math.max(scaleX, MIN_TEXT_XSCALE));
+ wordView.setTextScaleX(scaleX);
// A <code>wordView</code> should be disabled when <code>word</code> is empty in order to
// make it unclickable.
// With accessibility touch exploration on, <code>wordView</code> should be enabled even
@@ -548,22 +540,23 @@ final class SuggestionStripLayoutHelper {
return countInStrip;
}
- public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip) {
- final boolean shouldShowUiToAcceptTypedWord = Settings.getInstance().getCurrent()
- .mShouldShowUiToAcceptTypedWord;
+ public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip,
+ final boolean shouldShowWordToSave) {
+ final boolean showsHintWithWord = shouldShowWordToSave
+ || !Settings.getInstance().getCurrent().mShouldShowLxxSuggestionUi;
final int stripWidth = addToDictionaryStrip.getWidth();
- final int width = shouldShowUiToAcceptTypedWord ? stripWidth
- : stripWidth - mDividerWidth - mPadding * 2;
+ final int width = stripWidth - (showsHintWithWord ? mDividerWidth + mPadding * 2 : 0);
final TextView wordView = (TextView)addToDictionaryStrip.findViewById(R.id.word_to_save);
wordView.setTextColor(mColorTypedWord);
final int wordWidth = (int)(width * mCenterSuggestionWeight);
- final CharSequence wordToSave = getEllipsizedText(word, wordWidth, wordView.getPaint());
+ final CharSequence wordToSave = getEllipsizedTextWithSettingScaleX(
+ word, wordWidth, wordView.getPaint());
final float wordScaleX = wordView.getTextScaleX();
wordView.setText(wordToSave);
wordView.setTextScaleX(wordScaleX);
setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
- final int wordVisibility = shouldShowUiToAcceptTypedWord ? View.GONE : View.VISIBLE;
+ final int wordVisibility = showsHintWithWord ? View.VISIBLE : View.GONE;
wordView.setVisibility(wordVisibility);
addToDictionaryStrip.findViewById(R.id.word_to_save_divider).setVisibility(wordVisibility);
@@ -573,12 +566,7 @@ final class SuggestionStripLayoutHelper {
final float hintWeight;
final TextView hintView = (TextView)addToDictionaryStrip.findViewById(
R.id.hint_add_to_dictionary);
- if (shouldShowUiToAcceptTypedWord) {
- hintText = res.getText(R.string.hint_add_to_dictionary_without_word);
- hintWidth = width;
- hintWeight = 1.0f;
- hintView.setGravity(Gravity.CENTER);
- } else {
+ if (showsHintWithWord) {
final boolean isRtlLanguage = (ViewCompat.getLayoutDirection(addToDictionaryStrip)
== ViewCompat.LAYOUT_DIRECTION_RTL);
final String arrow = isRtlLanguage ? RIGHTWARDS_ARROW : LEFTWARDS_ARROW;
@@ -589,10 +577,15 @@ final class SuggestionStripLayoutHelper {
hintWidth = width - wordWidth;
hintWeight = 1.0f - mCenterSuggestionWeight;
hintView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
+ } else {
+ hintText = res.getText(R.string.hint_add_to_dictionary_without_word);
+ hintWidth = width;
+ hintWeight = 1.0f;
+ hintView.setGravity(Gravity.CENTER);
}
hintView.setTextColor(mColorAutoCorrect);
final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint());
- hintView.setText(hintText);
+ hintView.setText(hintText); // TextView.setText() resets text scale x to 1.0.
hintView.setTextScaleX(hintScaleX);
setLayoutWeight(hintView, hintWeight, ViewGroup.LayoutParams.MATCH_PARENT);
}
@@ -604,8 +597,7 @@ final class SuggestionStripLayoutHelper {
final int width = titleView.getWidth() - titleView.getPaddingLeft()
- titleView.getPaddingRight();
titleView.setTextColor(mColorAutoCorrect);
- titleView.setText(importantNoticeTitle);
- titleView.setTextScaleX(1.0f); // Reset textScaleX.
+ titleView.setText(importantNoticeTitle); // TextView.setText() resets text scale x to 1.0.
final float titleScaleX = getTextScaleX(importantNoticeTitle, width, titleView.getPaint());
titleView.setTextScaleX(titleScaleX);
}
@@ -620,18 +612,19 @@ final class SuggestionStripLayoutHelper {
}
}
- private static float getTextScaleX(final CharSequence text, final int maxWidth,
+ private static float getTextScaleX(@Nullable final CharSequence text, final int maxWidth,
final TextPaint paint) {
paint.setTextScaleX(1.0f);
final int width = getTextWidth(text, paint);
if (width <= maxWidth || maxWidth <= 0) {
return 1.0f;
}
- return maxWidth / (float)width;
+ return maxWidth / (float) width;
}
- private static CharSequence getEllipsizedText(final CharSequence text, final int maxWidth,
- final TextPaint paint) {
+ @Nullable
+ private static CharSequence getEllipsizedTextWithSettingScaleX(
+ @Nullable final CharSequence text, final int maxWidth, @Nonnull final TextPaint paint) {
if (text == null) {
return null;
}
@@ -641,62 +634,63 @@ final class SuggestionStripLayoutHelper {
return text;
}
- // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To
- // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE).
- final float upscaledWidth = maxWidth / MIN_TEXT_XSCALE;
- CharSequence ellipsized = TextUtils.ellipsize(
- text, paint, upscaledWidth, TextUtils.TruncateAt.MIDDLE);
- // For an unknown reason, ellipsized seems to return a text that does indeed fit inside the
- // passed width according to paint.measureText, but not according to paint.getTextWidths.
- // But when rendered, the text seems to actually take up as many pixels as returned by
- // paint.getTextWidths, hence problem.
- // To save this case, we compare the measured size of the new text, and if it's too much,
- // try it again removing the difference. This may still give a text too long by one or
- // two pixels so we take an additional 2 pixels cushion and call it a day.
- // TODO: figure out why getTextWidths and measureText don't agree with each other, and
- // remove the following code.
- final float ellipsizedTextWidth = getTextWidth(ellipsized, paint);
- if (upscaledWidth <= ellipsizedTextWidth) {
- ellipsized = TextUtils.ellipsize(
- text, paint, upscaledWidth - (ellipsizedTextWidth - upscaledWidth) - 2,
- TextUtils.TruncateAt.MIDDLE);
- }
+ // <code>text</code> must be ellipsized with minimum text scale x.
paint.setTextScaleX(MIN_TEXT_XSCALE);
- return ellipsized;
+ final boolean hasBoldStyle = hasStyleSpan(text, BOLD_SPAN);
+ final boolean hasUnderlineStyle = hasStyleSpan(text, UNDERLINE_SPAN);
+ // TextUtils.ellipsize erases any span object existed after ellipsized point.
+ // We have to restore these spans afterward.
+ final CharSequence ellipsizedText = TextUtils.ellipsize(
+ text, paint, maxWidth, TextUtils.TruncateAt.MIDDLE);
+ if (!hasBoldStyle && !hasUnderlineStyle) {
+ return ellipsizedText;
+ }
+ final Spannable spannableText = (ellipsizedText instanceof Spannable)
+ ? (Spannable)ellipsizedText : new SpannableString(ellipsizedText);
+ if (hasBoldStyle) {
+ addStyleSpan(spannableText, BOLD_SPAN);
+ }
+ if (hasUnderlineStyle) {
+ addStyleSpan(spannableText, UNDERLINE_SPAN);
+ }
+ return spannableText;
+ }
+
+ private static boolean hasStyleSpan(@Nullable final CharSequence text,
+ final CharacterStyle style) {
+ if (text instanceof Spanned) {
+ return ((Spanned)text).getSpanStart(style) >= 0;
+ }
+ return false;
+ }
+
+ private static void addStyleSpan(@Nonnull final Spannable text, final CharacterStyle style) {
+ text.removeSpan(style);
+ text.setSpan(style, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
- private static int getTextWidth(final CharSequence text, final TextPaint paint) {
+ private static int getTextWidth(@Nullable final CharSequence text, final TextPaint paint) {
if (TextUtils.isEmpty(text)) {
return 0;
}
+ final int length = text.length();
+ final float[] widths = new float[length];
+ final int count;
final Typeface savedTypeface = paint.getTypeface();
- paint.setTypeface(getTextTypeface(text));
- final int len = text.length();
- final float[] widths = new float[len];
- final int count = paint.getTextWidths(text, 0, len, widths);
+ try {
+ paint.setTypeface(getTextTypeface(text));
+ count = paint.getTextWidths(text, 0, length, widths);
+ } finally {
+ paint.setTypeface(savedTypeface);
+ }
int width = 0;
for (int i = 0; i < count; i++) {
width += Math.round(widths[i] + 0.5f);
}
- paint.setTypeface(savedTypeface);
return width;
}
- private static Typeface getTextTypeface(final CharSequence text) {
- if (!(text instanceof SpannableString)) {
- return Typeface.DEFAULT;
- }
-
- final SpannableString ss = (SpannableString)text;
- final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class);
- if (styles.length == 0) {
- return Typeface.DEFAULT;
- }
-
- if (styles[0].getStyle() == Typeface.BOLD) {
- return Typeface.DEFAULT_BOLD;
- }
- // TODO: BOLD_ITALIC, ITALIC case?
- return Typeface.DEFAULT;
+ private static Typeface getTextTypeface(@Nullable final CharSequence text) {
+ return hasStyleSpan(text, BOLD_SPAN) ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;
}
}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index 0fd5e139e..b71bd1f50 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -43,10 +43,10 @@ import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.keyboard.MoreKeysPanel;
import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
-import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.define.DebugFlags;
import com.android.inputmethod.latin.settings.Settings;
import com.android.inputmethod.latin.settings.SettingsValues;
@@ -82,7 +82,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
private final ArrayList<View> mDividerViews = new ArrayList<>();
Listener mListener;
- private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
+ private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance();
private int mStartIndexOfMoreSuggestions;
private final SuggestionStripLayoutHelper mLayoutHelper;
@@ -131,6 +131,10 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
mImportantNoticeStrip.setVisibility(VISIBLE);
}
+ public boolean isShowingImportantNoticeStrip() {
+ return mImportantNoticeStrip.getVisibility() == VISIBLE;
+ }
+
public boolean isShowingAddToDictionaryStrip() {
return mAddToDictionaryStrip.getVisibility() == VISIBLE;
}
@@ -227,8 +231,8 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
return mStripVisibilityGroup.isShowingAddToDictionaryStrip();
}
- public void showAddToDictionaryHint(final String word) {
- mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip);
+ public void showAddToDictionaryHint(final String word, final boolean shouldShowWordToSave) {
+ mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip, shouldShowWordToSave);
// {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word
// will be extracted at {@link #onClick(View)}.
mAddToDictionaryStrip.setTag(word);
@@ -340,12 +344,6 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
if (mSuggestedWords.size() <= mStartIndexOfMoreSuggestions) {
return false;
}
- // Dismiss another {@link MoreKeysPanel} that may be being showed, for example
- // {@link MoreKeysKeyboardView}.
- mMainKeyboardView.onDismissMoreKeysPanel();
- // Dismiss all key previews and sliding key input preview that may be being showed.
- mMainKeyboardView.dismissAllKeyPreviews();
- mMainKeyboardView.dismissSlidingKeyInputPreview();
final int stripWidth = getWidth();
final View container = mMoreSuggestionsContainer;
final int maxWidth = stripWidth - container.getPaddingLeft() - container.getPaddingRight();
@@ -393,11 +391,18 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
@Override
public boolean onInterceptTouchEvent(final MotionEvent me) {
+ if (mStripVisibilityGroup.isShowingImportantNoticeStrip()) {
+ return false;
+ }
+ // Detecting sliding up finger to show {@link MoreSuggestionsView}.
if (!mMoreSuggestionsView.isShowingInParent()) {
mLastX = (int)me.getX();
mLastY = (int)me.getY();
return mMoreSuggestionsSlidingDetector.onTouchEvent(me);
}
+ if (mMoreSuggestionsView.isInModalMode()) {
+ return false;
+ }
final int action = me.getAction();
final int index = me.getActionIndex();
@@ -416,7 +421,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
// Decided to be in the modal input mode.
- mMoreSuggestionsView.adjustVerticalCorrectionForModalMode();
+ mMoreSuggestionsView.setModalMode();
}
return false;
}
@@ -429,6 +434,11 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
@Override
public boolean onTouchEvent(final MotionEvent me) {
+ if (!mMoreSuggestionsView.isShowingInParent()) {
+ // Ignore any touch event while more suggestions panel hasn't been shown.
+ // Detecting sliding up is done at {@link #onInterceptTouchEvent}.
+ return true;
+ }
// In the sliding input mode. {@link MotionEvent} should be forwarded to
// {@link MoreSuggestionsView}.
final int index = me.getActionIndex();
@@ -485,7 +495,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
return;
}
final Object tag = view.getTag();
- // {@link String} tag is set at {@link #showAddToDictionaryHint(String,CharSequence)}.
+ // {@link String} tag is set at {@link #suggestAddingToDictionary(String,CharSequence)}.
if (tag instanceof String) {
final String wordToSave = (String)tag;
mListener.addWordToUserDictionary(wordToSave);
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java
index 52708455e..5c86a02af 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java
@@ -22,7 +22,7 @@ import com.android.inputmethod.latin.SuggestedWords;
* An object that gives basic control of a suggestion strip and some info on it.
*/
public interface SuggestionStripViewAccessor {
- public void showAddToDictionaryHint(final String word);
+ public void suggestAddingToDictionary(final String word, final boolean isFromSuggestionStrip);
public boolean isShowingAddToDictionaryHint();
public void dismissAddToDictionaryHint();
public void setNeutralSuggestionStrip();
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
index 624783a70..90e4faafd 100644
--- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
@@ -47,12 +47,12 @@ public class UserDictionaryList extends PreferenceFragment {
"android.settings.USER_DICTIONARY_SETTINGS";
@Override
- public void onCreate(Bundle icicle) {
+ public void onCreate(final Bundle icicle) {
super.onCreate(icicle);
setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getActivity()));
}
- public static TreeSet<String> getUserDictionaryLocalesSet(Activity activity) {
+ public static TreeSet<String> getUserDictionaryLocalesSet(final Activity activity) {
final Cursor cursor = activity.getContentResolver().query(UserDictionary.Words.CONTENT_URI,
new String[] { UserDictionary.Words.LOCALE },
null, null, null);
@@ -108,7 +108,7 @@ public class UserDictionaryList extends PreferenceFragment {
* Creates the entries that allow the user to go into the user dictionary for each locale.
* @param userDictGroup The group to put the settings in.
*/
- protected void createUserDictSettings(PreferenceGroup userDictGroup) {
+ protected void createUserDictSettings(final PreferenceGroup userDictGroup) {
final Activity activity = getActivity();
userDictGroup.removeAll();
final TreeSet<String> localeSet =
@@ -121,10 +121,10 @@ public class UserDictionaryList extends PreferenceFragment {
}
if (localeSet.isEmpty()) {
- userDictGroup.addPreference(createUserDictionaryPreference(null, activity));
+ userDictGroup.addPreference(createUserDictionaryPreference(null));
} else {
for (String locale : localeSet) {
- userDictGroup.addPreference(createUserDictionaryPreference(locale, activity));
+ userDictGroup.addPreference(createUserDictionaryPreference(locale));
}
}
}
@@ -134,7 +134,7 @@ public class UserDictionaryList extends PreferenceFragment {
* @param locale The locale for which this user dictionary is for.
* @return The corresponding preference.
*/
- protected Preference createUserDictionaryPreference(String locale, Activity activity) {
+ protected Preference createUserDictionaryPreference(final String locale) {
final Preference newPref = new Preference(getActivity());
final Intent intent = new Intent(USER_DICTIONARY_SETTINGS_INTENT_ACTION);
if (null == locale) {
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
index cf2014a1a..1d7e7d683 100644
--- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
@@ -177,17 +177,16 @@ public class UserDictionarySettings extends ListFragment {
return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION,
QUERY_SELECTION_ALL_LOCALES, null,
"UPPER(" + UserDictionary.Words.WORD + ")");
- } else {
- final String queryLocale = null != locale ? locale : Locale.getDefault().toString();
- return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION,
- QUERY_SELECTION, new String[] { queryLocale },
- "UPPER(" + UserDictionary.Words.WORD + ")");
}
+ final String queryLocale = null != locale ? locale : Locale.getDefault().toString();
+ return getActivity().managedQuery(UserDictionary.Words.CONTENT_URI, QUERY_PROJECTION,
+ QUERY_SELECTION, new String[] { queryLocale },
+ "UPPER(" + UserDictionary.Words.WORD + ")");
}
private ListAdapter createAdapter() {
return new MyAdapter(getActivity(), R.layout.user_dictionary_item, mCursor,
- ADAPTER_FROM, ADAPTER_TO, this);
+ ADAPTER_FROM, ADAPTER_TO);
}
@Override
@@ -283,13 +282,12 @@ public class UserDictionarySettings extends ListFragment {
}
private static class MyAdapter extends SimpleCursorAdapter implements SectionIndexer {
-
private AlphabetIndexer mIndexer;
private ViewBinder mViewBinder = new ViewBinder() {
@Override
- public boolean setViewValue(View v, Cursor c, int columnIndex) {
+ public boolean setViewValue(final View v, final Cursor c, final int columnIndex) {
if (!IS_SHORTCUT_API_SUPPORTED) {
// just let SimpleCursorAdapter set the view values
return false;
@@ -310,10 +308,9 @@ public class UserDictionarySettings extends ListFragment {
}
};
- @SuppressWarnings("deprecation")
- public MyAdapter(Context context, int layout, Cursor c, String[] from, int[] to,
- UserDictionarySettings settings) {
- super(context, layout, c, from, to);
+ public MyAdapter(final Context context, final int layout, final Cursor c,
+ final String[] from, final int[] to) {
+ super(context, layout, c, from, to, 0 /* flags */);
if (null != c) {
final String alphabet = context.getString(R.string.user_dict_fast_scroll_alphabet);
@@ -324,12 +321,12 @@ public class UserDictionarySettings extends ListFragment {
}
@Override
- public int getPositionForSection(int section) {
+ public int getPositionForSection(final int section) {
return null == mIndexer ? 0 : mIndexer.getPositionForSection(section);
}
@Override
- public int getSectionForPosition(int position) {
+ public int getSectionForPosition(final int position) {
return null == mIndexer ? 0 : mIndexer.getSectionForPosition(position);
}
diff --git a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
index db7f2a56c..2aac7c57a 100644
--- a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
@@ -16,12 +16,12 @@
package com.android.inputmethod.latin.utils;
-import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE;
-import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE;
-import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
-import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE;
-import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
-import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
+import static com.android.inputmethod.latin.common.Constants.Subtype.KEYBOARD_MODE;
+import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.ASCII_CAPABLE;
+import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
+import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE;
+import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
import android.os.Build;
import android.text.TextUtils;
@@ -31,6 +31,7 @@ import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.common.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
index d12aad639..952ac2a62 100644
--- a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
+++ b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
@@ -59,11 +59,7 @@ public class AsyncResultHolder<E> {
*/
public E get(final E defaultValue, final long timeOut) {
try {
- if (mLatch.await(timeOut, TimeUnit.MILLISECONDS)) {
- return mResult;
- } else {
- return defaultValue;
- }
+ return mLatch.await(timeOut, TimeUnit.MILLISECONDS) ? mResult : defaultValue;
} catch (InterruptedException e) {
return defaultValue;
}
diff --git a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
index 156fcf57c..120cffbde 100644
--- a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
@@ -24,7 +24,6 @@ import com.android.inputmethod.latin.define.DebugFlags;
public final class AutoCorrectionUtils {
private static final boolean DBG = DebugFlags.DEBUG_ENABLED;
private static final String TAG = AutoCorrectionUtils.class.getSimpleName();
- private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
private AutoCorrectionUtils() {
// Purely static class: can't instantiate.
@@ -52,41 +51,9 @@ public final class AutoCorrectionUtils {
if (DBG) {
Log.d(TAG, "Auto corrected by S-threshold.");
}
- return !shouldBlockAutoCorrectionBySafetyNet(consideredWord, suggestion.mWord);
+ return true;
}
}
return false;
}
-
- // TODO: Resolve the inconsistencies between the native auto correction algorithms and
- // this safety net
- public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord,
- final String 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 / 2) + 1;
- final int distance = BinaryDictionaryUtils.editDistance(typedWord, suggestion);
- 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/utils/BinaryDictionaryUtils.java b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java
index 5d7deba15..23ffde2a2 100644
--- a/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java
@@ -18,6 +18,7 @@ package com.android.inputmethod.latin.utils;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.makedict.DictionaryHeader;
import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
import com.android.inputmethod.latin.personalization.PersonalizationHelper;
@@ -43,7 +44,6 @@ public final class BinaryDictionaryUtils {
private static native boolean createEmptyDictFileNative(String filePath, long dictVersion,
String locale, String[] attributeKeyStringArray, String[] attributeValueStringArray);
private static native float calcNormalizedScoreNative(int[] before, int[] after, int score);
- private static native int editDistanceNative(int[] before, int[] after);
private static native int setCurrentTimeForTestNative(int currentTime);
public static DictionaryHeader getHeader(final File dictFile)
@@ -112,14 +112,6 @@ public final class BinaryDictionaryUtils {
StringUtils.toCodePointArray(after), score);
}
- public static int editDistance(final String before, final String after) {
- if (before == null || after == null) {
- throw new IllegalArgumentException();
- }
- return editDistanceNative(StringUtils.toCodePointArray(before),
- StringUtils.toCodePointArray(after));
- }
-
/**
* Control the current time to be used in the native code. If currentTime >= 0, this method sets
* the current time and gets into test mode.
diff --git a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
index 936219332..0dbc7c858 100644
--- a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
@@ -19,10 +19,12 @@ package com.android.inputmethod.latin.utils;
import android.text.InputType;
import android.text.TextUtils;
-import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
+import java.util.ArrayList;
import java.util.Locale;
public final class CapsModeUtils {
@@ -213,12 +215,22 @@ public final class CapsModeUtils {
char c = cs.charAt(--j);
// We found the next interesting chunk of text ; next we need to determine if it's the
- // end of a sentence. If we have a question mark or an exclamation mark, it's the end of
- // a sentence. If it's neither, the only remaining case is the period so we get the opposite
- // case out of the way.
- if (c == Constants.CODE_QUESTION_MARK || c == Constants.CODE_EXCLAMATION_MARK) {
- return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes;
+ // end of a sentence. If we have a sentence terminator (typically a question mark or an
+ // exclamation mark), then it's the end of a sentence; however, we treat the abbreviation
+ // marker specially because usually is the same char as the sentence separator (the
+ // period in most languages) and in this case we need to apply a heuristic to determine
+ // in which of these senses it's used.
+ if (spacingAndPunctuations.isSentenceTerminator(c)
+ && !spacingAndPunctuations.isAbbreviationMarker(c)) {
+ return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
+ | TextUtils.CAP_MODE_SENTENCES) & reqModes;
}
+ // If we reach here, we know we have whitespace before the cursor and before that there
+ // is something that either does not terminate the sentence, or a symbol preceded by the
+ // start of the text, or it's the sentence separator AND it happens to be the same code
+ // point as the abbreviation marker.
+ // If it's a symbol or something that does not terminate the sentence, then we need to
+ // return caps for MODE_CHARACTERS and MODE_WORDS, but not for MODE_SENTENCES.
if (!spacingAndPunctuations.isSentenceSeparator(c) || j <= 0) {
return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
}
@@ -315,4 +327,31 @@ public final class CapsModeUtils {
// Here we arrived at the start of the line. This should behave exactly like whitespace.
return (START == state || LETTER == state) ? noCaps : caps;
}
+
+ /**
+ * Convert capitalize mode flags into human readable text.
+ *
+ * @param capsFlags The modes flags to be converted. It may be any combination of
+ * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
+ * {@link TextUtils#CAP_MODE_SENTENCES}.
+ * @return the text that describe the <code>capsMode</code>.
+ */
+ public static String flagsToString(final int capsFlags) {
+ final int capsFlagsMask = TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
+ | TextUtils.CAP_MODE_SENTENCES;
+ if ((capsFlags & ~capsFlagsMask) != 0) {
+ return "unknown<0x" + Integer.toHexString(capsFlags) + ">";
+ }
+ final ArrayList<String> builder = new ArrayList<>();
+ if ((capsFlags & android.text.TextUtils.CAP_MODE_CHARACTERS) != 0) {
+ builder.add("characters");
+ }
+ if ((capsFlags & android.text.TextUtils.CAP_MODE_WORDS) != 0) {
+ builder.add("words");
+ }
+ if ((capsFlags & android.text.TextUtils.CAP_MODE_SENTENCES) != 0) {
+ builder.add("sentences");
+ }
+ return builder.isEmpty() ? "none" : TextUtils.join("|", builder);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
index 61292fc36..01f5e1079 100644
--- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
@@ -17,21 +17,32 @@
package com.android.inputmethod.latin.utils;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Map;
-import java.util.TreeMap;
+import java.util.Collection;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Utility methods for working with collections.
+ */
public final class CollectionUtils {
private CollectionUtils() {
// This utility class is not publicly instantiable.
}
- public static <E> ArrayList<E> arrayAsList(final E[] array, final int start, final int end) {
- if (array == null) {
- throw new NullPointerException();
- }
+ /**
+ * Converts a sub-range of the given array to an ArrayList of the appropriate type.
+ * @param array Array to be converted.
+ * @param start First index inclusive to be converted.
+ * @param end Last index exclusive to be converted.
+ * @throws IllegalArgumentException if start or end are out of range or start &gt; end.
+ */
+ @Nonnull
+ public static <E> ArrayList<E> arrayAsList(@Nonnull final E[] array, final int start,
+ final int end) {
if (start < 0 || start > end || end > array.length) {
- throw new IllegalArgumentException();
+ throw new IllegalArgumentException("Invalid start: " + start + " end: " + end
+ + " with array.length: " + array.length);
}
final ArrayList<E> list = new ArrayList<>(end - start);
@@ -40,4 +51,13 @@ public final class CollectionUtils {
}
return list;
}
+
+ /**
+ * Tests whether c contains no elements, true if c is null or c is empty.
+ * @param c Collection to test.
+ * @return Whether c contains no elements.
+ */
+ public static boolean isNullOrEmpty(@Nullable final Collection<?> c) {
+ return c == null || c.isEmpty();
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java
index 34f59e8bc..8699f2ce7 100644
--- a/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java
@@ -17,6 +17,7 @@
package com.android.inputmethod.latin.utils;
import com.android.inputmethod.latin.makedict.DictionaryHeader;
+import com.android.inputmethod.latin.makedict.NgramProperty;
import com.android.inputmethod.latin.makedict.ProbabilityInfo;
import com.android.inputmethod.latin.makedict.WeightedString;
import com.android.inputmethod.latin.makedict.WordProperty;
@@ -26,6 +27,8 @@ import java.util.HashMap;
public class CombinedFormatUtils {
public static final String DICTIONARY_TAG = "dictionary";
public static final String BIGRAM_TAG = "bigram";
+ public static final String NGRAM_TAG = "ngram";
+ public static final String NGRAM_PREV_WORD_TAG = "prev_word";
public static final String SHORTCUT_TAG = "shortcut";
public static final String PROBABILITY_TAG = "f";
public static final String HISTORICAL_INFO_TAG = "historicalInfo";
@@ -33,7 +36,8 @@ public class CombinedFormatUtils {
public static final String WORD_TAG = "word";
public static final String BEGINNING_OF_SENTENCE_TAG = "beginning_of_sentence";
public static final String NOT_A_WORD_TAG = "not_a_word";
- public static final String BLACKLISTED_TAG = "blacklisted";
+ public static final String POSSIBLY_OFFENSIVE_TAG = "possibly_offensive";
+ public static final String TRUE_VALUE = "true";
public static String formatAttributeMap(final HashMap<String, String> attributeMap) {
final StringBuilder builder = new StringBuilder();
@@ -58,16 +62,16 @@ public class CombinedFormatUtils {
builder.append(",");
builder.append(formatProbabilityInfo(wordProperty.mProbabilityInfo));
if (wordProperty.mIsBeginningOfSentence) {
- builder.append("," + BEGINNING_OF_SENTENCE_TAG + "=true");
+ builder.append("," + BEGINNING_OF_SENTENCE_TAG + "=" + TRUE_VALUE);
}
if (wordProperty.mIsNotAWord) {
- builder.append("," + NOT_A_WORD_TAG + "=true");
+ builder.append("," + NOT_A_WORD_TAG + "=" + TRUE_VALUE);
}
- if (wordProperty.mIsBlacklistEntry) {
- builder.append("," + BLACKLISTED_TAG + "=true");
+ if (wordProperty.mIsPossiblyOffensive) {
+ builder.append("," + POSSIBLY_OFFENSIVE_TAG + "=" + TRUE_VALUE);
}
builder.append("\n");
- if (wordProperty.mShortcutTargets != null) {
+ if (wordProperty.mHasShortcuts) {
for (final WeightedString shortcutTarget : wordProperty.mShortcutTargets) {
builder.append(" " + SHORTCUT_TAG + "=" + shortcutTarget.mWord);
builder.append(",");
@@ -75,12 +79,20 @@ public class CombinedFormatUtils {
builder.append("\n");
}
}
- if (wordProperty.mBigrams != null) {
- for (final WeightedString bigram : wordProperty.mBigrams) {
- builder.append(" " + BIGRAM_TAG + "=" + bigram.mWord);
+ if (wordProperty.mHasNgrams) {
+ for (final NgramProperty ngramProperty : wordProperty.mNgrams) {
+ builder.append(" " + NGRAM_TAG + "=" + ngramProperty.mTargetWord.mWord);
builder.append(",");
- builder.append(formatProbabilityInfo(bigram.mProbabilityInfo));
+ builder.append(formatProbabilityInfo(ngramProperty.mTargetWord.mProbabilityInfo));
builder.append("\n");
+ for (int i = 0; i < ngramProperty.mNgramContext.getPrevWordCount(); i++) {
+ builder.append(" " + NGRAM_PREV_WORD_TAG + "[" + i + "]="
+ + ngramProperty.mNgramContext.getNthPrevWord(i + 1));
+ if (ngramProperty.mNgramContext.isNthPrevWordBeginningOfSontence(i + 1)) {
+ builder.append("," + BEGINNING_OF_SENTENCE_TAG + "=true");
+ }
+ builder.append("\n");
+ }
}
}
return builder.toString();
@@ -100,4 +112,8 @@ public class CombinedFormatUtils {
}
return builder.toString();
}
+
+ public static boolean isLiteralTrue(final String value) {
+ return TRUE_VALUE.equalsIgnoreCase(value);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java b/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java
index 87df013a6..3a9705904 100644
--- a/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java
@@ -16,7 +16,7 @@
package com.android.inputmethod.latin.utils;
-import java.util.Arrays;
+import javax.annotation.Nonnull;
public final class CoordinateUtils {
private static final int INDEX_X = 0;
@@ -27,32 +27,35 @@ public final class CoordinateUtils {
// This utility class is not publicly instantiable.
}
+ @Nonnull
public static int[] newInstance() {
return new int[ELEMENT_SIZE];
}
- public static int x(final int[] coords) {
+ public static int x(@Nonnull final int[] coords) {
return coords[INDEX_X];
}
- public static int y(final int[] coords) {
+ public static int y(@Nonnull final int[] coords) {
return coords[INDEX_Y];
}
- public static void set(final int[] coords, final int x, final int y) {
+ public static void set(@Nonnull final int[] coords, final int x, final int y) {
coords[INDEX_X] = x;
coords[INDEX_Y] = y;
}
- public static void copy(final int[] destination, final int[] source) {
+ public static void copy(@Nonnull final int[] destination, @Nonnull final int[] source) {
destination[INDEX_X] = source[INDEX_X];
destination[INDEX_Y] = source[INDEX_Y];
}
+ @Nonnull
public static int[] newCoordinateArray(final int arraySize) {
return new int[ELEMENT_SIZE * arraySize];
}
+ @Nonnull
public static int[] newCoordinateArray(final int arraySize,
final int defaultX, final int defaultY) {
final int[] result = new int[ELEMENT_SIZE * arraySize];
@@ -62,30 +65,30 @@ public final class CoordinateUtils {
return result;
}
- public static int xFromArray(final int[] coordsArray, final int index) {
+ public static int xFromArray(@Nonnull final int[] coordsArray, final int index) {
return coordsArray[ELEMENT_SIZE * index + INDEX_X];
}
- public static int yFromArray(final int[] coordsArray, final int index) {
+ public static int yFromArray(@Nonnull final int[] coordsArray, final int index) {
return coordsArray[ELEMENT_SIZE * index + INDEX_Y];
}
- public static int[] coordinateFromArray(final int[] coordsArray, final int index) {
- final int baseIndex = ELEMENT_SIZE * index;
- return Arrays.copyOfRange(coordsArray, baseIndex, baseIndex + ELEMENT_SIZE);
+ @Nonnull
+ public static int[] coordinateFromArray(@Nonnull final int[] coordsArray, final int index) {
+ final int[] coords = newInstance();
+ set(coords, xFromArray(coordsArray, index), yFromArray(coordsArray, index));
+ return coords;
}
- public static void setXYInArray(final int[] coordsArray, final int index,
+ public static void setXYInArray(@Nonnull final int[] coordsArray, final int index,
final int x, final int y) {
final int baseIndex = ELEMENT_SIZE * index;
coordsArray[baseIndex + INDEX_X] = x;
coordsArray[baseIndex + INDEX_Y] = y;
}
- public static void setCoordinateInArray(final int[] coordsArray, final int index,
- final int[] coords) {
- final int baseIndex = ELEMENT_SIZE * index;
- coordsArray[baseIndex + INDEX_X] = coords[INDEX_X];
- coordsArray[baseIndex + INDEX_Y] = coords[INDEX_Y];
+ public static void setCoordinateInArray(@Nonnull final int[] coordsArray, final int index,
+ @Nonnull final int[] coords) {
+ setXYInArray(coordsArray, index, x(coords), y(coords));
}
}
diff --git a/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java
index 9dc0524a2..c90d30c42 100644
--- a/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java
@@ -16,17 +16,26 @@
package com.android.inputmethod.latin.utils;
+import android.annotation.TargetApi;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.inputmethodservice.ExtractEditText;
import android.inputmethodservice.InputMethodService;
+import android.os.Build;
import android.text.Layout;
import android.text.Spannable;
+import android.text.Spanned;
import android.view.View;
import android.view.ViewParent;
import android.view.inputmethod.CursorAnchorInfo;
import android.widget.TextView;
+import com.android.inputmethod.compat.BuildCompatUtils;
+import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
/**
* This class allows input methods to extract {@link CursorAnchorInfo} directly from the given
* {@link TextView}. This is useful and even necessary to support full-screen mode where the default
@@ -77,13 +86,32 @@ public final class CursorAnchorInfoUtils {
}
/**
+ * Extracts {@link CursorAnchorInfoCompatWrapper} from the given {@link TextView}.
+ * @param textView the target text view from which {@link CursorAnchorInfoCompatWrapper} is to
+ * be extracted.
+ * @return the {@link CursorAnchorInfoCompatWrapper} object based on the current layout.
+ * {@code null} if {@code Build.VERSION.SDK_INT} is 20 or prior or {@link TextView} is not
+ * ready to provide layout information.
+ */
+ @Nullable
+ public static CursorAnchorInfoCompatWrapper extractFromTextView(
+ @Nonnull final TextView textView) {
+ if (BuildCompatUtils.EFFECTIVE_SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return null;
+ }
+ return CursorAnchorInfoCompatWrapper.wrap(extractFromTextViewInternal(textView));
+ }
+
+ /**
* Returns {@link CursorAnchorInfo} from the given {@link TextView}.
* @param textView the target text view from which {@link CursorAnchorInfo} is to be extracted.
* @return the {@link CursorAnchorInfo} object based on the current layout. {@code null} if it
* is not feasible.
*/
- public static CursorAnchorInfo getCursorAnchorInfo(final TextView textView) {
- Layout layout = textView.getLayout();
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Nullable
+ private static CursorAnchorInfo extractFromTextViewInternal(@Nonnull final TextView textView) {
+ final Layout layout = textView.getLayout();
if (layout == null) {
return null;
}
@@ -122,7 +150,7 @@ public final class CursorAnchorInfoUtils {
final Object[] spans = spannable.getSpans(0, text.length(), Object.class);
for (Object span : spans) {
final int spanFlag = spannable.getSpanFlags(span);
- if ((spanFlag & Spannable.SPAN_COMPOSING) != 0) {
+ if ((spanFlag & Spanned.SPAN_COMPOSING) != 0) {
composingTextStart = Math.min(composingTextStart,
spannable.getSpanStart(span));
composingTextEnd = Math.max(composingTextEnd, spannable.getSpanEnd(span));
diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
index 197908032..24025b272 100644
--- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
@@ -23,10 +23,11 @@ import android.content.res.Resources;
import android.text.TextUtils;
import android.util.Log;
+import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.AssetFileAddress;
import com.android.inputmethod.latin.BinaryDictionaryGetter;
-import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.makedict.DictionaryHeader;
import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
@@ -229,6 +230,7 @@ public class DictionaryInfoUtils {
/**
* Helper method to return a dictionary res id for a locale, or 0 if none.
+ * @param res resources for the app
* @param locale dictionary locale
* @return main dictionary resource id
*/
@@ -257,6 +259,7 @@ public class DictionaryInfoUtils {
/**
* Returns a main dictionary resource id
+ * @param res resources for the app
* @param locale dictionary locale
* @return main dictionary resource id
*/
@@ -282,6 +285,25 @@ public class DictionaryInfoUtils {
BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.getLanguage().toString();
}
+ /**
+ * Returns whether a main dictionary is readily available for this locale.
+ *
+ * This does not query the content provider.
+ *
+ * @param context context to open files upon
+ * @param locale dictionary locale
+ * @return true if a dictionary is available right away, false otherwise
+ */
+ public static boolean hasReadilyAvailableMainDictionaryForLocale(final Context context,
+ final Locale locale) {
+ final Resources res = context.getResources();
+ if (0 != getMainDictionaryResourceIdIfAvailableForLocale(res, locale)) {
+ return true;
+ }
+ final String fileName = getCacheFileName(getMainDictId(locale), locale.toString(), context);
+ return new File(fileName).exists();
+ }
+
public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file) {
return getDictionaryFileHeaderOrNull(file, 0, file.length());
}
@@ -382,6 +404,7 @@ public class DictionaryInfoUtils {
return dictList;
}
+ @UsedForTesting
public static boolean looksValidForDictionaryInsertion(final CharSequence text,
final SpacingAndPunctuations spacingAndPunctuations) {
if (TextUtils.isEmpty(text)) return false;
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
index 787e4a59d..525212c96 100644
--- a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
@@ -16,38 +16,77 @@
package com.android.inputmethod.latin.utils;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.NgramContext;
+
import java.util.List;
import java.util.Locale;
-import android.view.inputmethod.InputMethodSubtype;
-
-import com.android.inputmethod.latin.PrevWordsInfo;
+import javax.annotation.Nonnull;
public interface DistracterFilter {
/**
* Determine whether a word is a distracter to words in dictionaries.
*
- * @param prevWordsInfo the information of previous words.
+ * @param ngramContext the n-gram context
* @param testedWord the word that will be tested to see whether it is a distracter to words
* in dictionaries.
* @param locale the locale of word.
* @return true if testedWord is a distracter, otherwise false.
*/
- public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo,
+ public boolean isDistracterToWordsInDictionaries(final NgramContext ngramContext,
final String testedWord, final Locale locale);
+ @UsedForTesting
+ public int getWordHandlingType(final NgramContext ngramContext, final String testedWord,
+ final Locale locale);
+
public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes);
public void close();
+ public static final class HandlingType {
+ private final static int REQUIRE_NO_SPECIAL_HANDLINGS = 0x0;
+ private final static int SHOULD_BE_LOWER_CASED = 0x1;
+ private final static int SHOULD_BE_HANDLED_AS_OOV = 0x2;
+
+ public static int getHandlingType(final boolean shouldBeLowerCased, final boolean isOov) {
+ int wordHandlingType = HandlingType.REQUIRE_NO_SPECIAL_HANDLINGS;
+ if (shouldBeLowerCased) {
+ wordHandlingType |= HandlingType.SHOULD_BE_LOWER_CASED;
+ }
+ if (isOov) {
+ wordHandlingType |= HandlingType.SHOULD_BE_HANDLED_AS_OOV;
+ }
+ return wordHandlingType;
+ }
+
+ public static boolean shouldBeLowerCased(final int handlingType) {
+ return (handlingType & SHOULD_BE_LOWER_CASED) != 0;
+ }
+
+ public static boolean shouldBeHandledAsOov(final int handlingType) {
+ return (handlingType & SHOULD_BE_HANDLED_AS_OOV) != 0;
+ }
+ }
+
+ @Nonnull
public static final DistracterFilter EMPTY_DISTRACTER_FILTER = new DistracterFilter() {
@Override
- public boolean isDistracterToWordsInDictionaries(PrevWordsInfo prevWordsInfo,
+ public boolean isDistracterToWordsInDictionaries(NgramContext ngramContext,
String testedWord, Locale locale) {
return false;
}
@Override
+ public int getWordHandlingType(final NgramContext ngramContext,
+ final String testedWord, final Locale locale) {
+ return HandlingType.REQUIRE_NO_SPECIAL_HANDLINGS;
+ }
+
+ @Override
public void close() {
}
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
index 27973287d..9c6a94810 100644
--- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
@@ -20,13 +20,14 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.ConcurrentHashMap;
import android.content.Context;
import android.content.res.Resources;
import android.text.InputType;
import android.util.Log;
import android.util.LruCache;
+import android.util.Pair;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
@@ -34,9 +35,12 @@ import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardId;
import com.android.inputmethod.keyboard.KeyboardLayoutSet;
import com.android.inputmethod.latin.DictionaryFacilitator;
-import com.android.inputmethod.latin.PrevWordsInfo;
+import com.android.inputmethod.latin.DictionaryFacilitatorLruCache;
+import com.android.inputmethod.latin.NgramContext;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
/**
@@ -48,21 +52,22 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr
DistracterFilterCheckingExactMatchesAndSuggestions.class.getSimpleName();
private static final boolean DEBUG = false;
- private static final long TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS = 120;
- private static final int MAX_DISTRACTERS_CACHE_SIZE = 512;
+ private static final int MAX_DICTIONARY_FACILITATOR_CACHE_SIZE = 3;
+ private static final int MAX_DISTRACTERS_CACHE_SIZE = 1024;
private final Context mContext;
- private final Map<Locale, InputMethodSubtype> mLocaleToSubtypeMap;
- private final Map<Locale, Keyboard> mLocaleToKeyboardMap;
- private final DictionaryFacilitator mDictionaryFacilitator;
- private final LruCache<String, Boolean> mDistractersCache;
- private Keyboard mKeyboard;
+ private final ConcurrentHashMap<Locale, InputMethodSubtype> mLocaleToSubtypeCache;
+ private final ConcurrentHashMap<Locale, Keyboard> mLocaleToKeyboardCache;
+ private final DictionaryFacilitatorLruCache mDictionaryFacilitatorLruCache;
+ // The key is a pair of a locale and a word. The value indicates the word is a distracter to
+ // words of the locale.
+ private final LruCache<Pair<Locale, String>, Boolean> mDistractersCache;
private final Object mLock = new Object();
// If the score of the top suggestion exceeds this value, the tested word (e.g.,
- // an OOV, a misspelling, or an in-vocabulary word) would be considered as a distractor to
+ // an OOV, a misspelling, or an in-vocabulary word) would be considered as a distracter to
// words in dictionary. The greater the threshold is, the less likely the tested word would
- // become a distractor, which means the tested word will be more likely to be added to
+ // become a distracter, which means the tested word will be more likely to be added to
// the dictionary.
private static final float DISTRACTER_WORD_SCORE_THRESHOLD = 0.4f;
@@ -73,16 +78,19 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr
*/
public DistracterFilterCheckingExactMatchesAndSuggestions(final Context context) {
mContext = context;
- mLocaleToSubtypeMap = new HashMap<>();
- mLocaleToKeyboardMap = new HashMap<>();
- mDictionaryFacilitator = new DictionaryFacilitator();
+ mLocaleToSubtypeCache = new ConcurrentHashMap<>();
+ mLocaleToKeyboardCache = new ConcurrentHashMap<>();
+ mDictionaryFacilitatorLruCache = new DictionaryFacilitatorLruCache(context,
+ MAX_DICTIONARY_FACILITATOR_CACHE_SIZE, "" /* dictionaryNamePrefix */);
mDistractersCache = new LruCache<>(MAX_DISTRACTERS_CACHE_SIZE);
- mKeyboard = null;
}
@Override
public void close() {
- mDictionaryFacilitator.closeDictionaries();
+ mLocaleToSubtypeCache.clear();
+ mLocaleToKeyboardCache.clear();
+ mDictionaryFacilitatorLruCache.evictAll();
+ // Don't clear mDistractersCache.
}
@Override
@@ -99,29 +107,36 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr
newLocaleToSubtypeMap.put(locale, subtype);
}
}
- if (mLocaleToSubtypeMap.equals(newLocaleToSubtypeMap)) {
+ if (mLocaleToSubtypeCache.equals(newLocaleToSubtypeMap)) {
// Enabled subtypes have not been changed.
return;
}
- synchronized (mLock) {
- mLocaleToSubtypeMap.clear();
- mLocaleToSubtypeMap.putAll(newLocaleToSubtypeMap);
- mLocaleToKeyboardMap.clear();
+ // Update subtype and keyboard map for locales that are in the current mapping.
+ for (final Locale locale: mLocaleToSubtypeCache.keySet()) {
+ if (newLocaleToSubtypeMap.containsKey(locale)) {
+ final InputMethodSubtype newSubtype = newLocaleToSubtypeMap.remove(locale);
+ if (newSubtype.equals(newLocaleToSubtypeMap.get(locale))) {
+ // Mapping has not been changed.
+ continue;
+ }
+ mLocaleToSubtypeCache.replace(locale, newSubtype);
+ } else {
+ mLocaleToSubtypeCache.remove(locale);
+ }
+ mLocaleToKeyboardCache.remove(locale);
}
+ // Add locales that are not in the current mapping.
+ mLocaleToSubtypeCache.putAll(newLocaleToSubtypeMap);
}
- private void loadKeyboardForLocale(final Locale newLocale) {
- final Keyboard cachedKeyboard = mLocaleToKeyboardMap.get(newLocale);
+ private Keyboard getKeyboardForLocale(final Locale locale) {
+ final Keyboard cachedKeyboard = mLocaleToKeyboardCache.get(locale);
if (cachedKeyboard != null) {
- mKeyboard = cachedKeyboard;
- return;
- }
- final InputMethodSubtype subtype;
- synchronized (mLock) {
- subtype = mLocaleToSubtypeMap.get(newLocale);
+ return cachedKeyboard;
}
+ final InputMethodSubtype subtype = mLocaleToSubtypeCache.get(locale);
if (subtype == null) {
- return;
+ return null;
}
final EditorInfo editorInfo = new EditorInfo();
editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
@@ -131,59 +146,41 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr
final int keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res);
final int keyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res);
builder.setKeyboardGeometry(keyboardWidth, keyboardHeight);
- builder.setSubtype(subtype);
+ builder.setSubtype(new RichInputMethodSubtype(subtype));
builder.setIsSpellChecker(false /* isSpellChecker */);
final KeyboardLayoutSet layoutSet = builder.build();
- mKeyboard = layoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET);
- }
-
- private void loadDictionariesForLocale(final Locale newlocale) throws InterruptedException {
- mDictionaryFacilitator.resetDictionaries(mContext, newlocale,
- false /* useContactsDict */, false /* usePersonalizedDicts */,
- false /* forceReloadMainDictionary */, null /* listener */);
- mDictionaryFacilitator.waitForLoadingMainDictionary(
- TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS, TimeUnit.SECONDS);
+ final Keyboard newKeyboard = layoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET);
+ mLocaleToKeyboardCache.put(locale, newKeyboard);
+ return newKeyboard;
}
/**
* Determine whether a word is a distracter to words in dictionaries.
*
- * @param prevWordsInfo the information of previous words. Not used for now.
+ * @param ngramContext the n-gram context. Not used for now.
* @param testedWord the word that will be tested to see whether it is a distracter to words
* in dictionaries.
* @param locale the locale of word.
* @return true if testedWord is a distracter, otherwise false.
*/
@Override
- public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo,
+ public boolean isDistracterToWordsInDictionaries(final NgramContext ngramContext,
final String testedWord, final Locale locale) {
if (locale == null) {
return false;
}
- if (!locale.equals(mDictionaryFacilitator.getLocale())) {
- synchronized (mLock) {
- if (!mLocaleToSubtypeMap.containsKey(locale)) {
- Log.e(TAG, "Locale " + locale + " is not enabled.");
- // TODO: Investigate what we should do for disabled locales.
- return false;
- }
- loadKeyboardForLocale(locale);
- // Reset dictionaries for the locale.
- try {
- mDistractersCache.evictAll();
- loadDictionariesForLocale(locale);
- } catch (final InterruptedException e) {
- Log.e(TAG, "Interrupted while waiting for loading dicts in DistracterFilter",
- e);
- return false;
- }
- }
+ if (!mLocaleToSubtypeCache.containsKey(locale)) {
+ Log.e(TAG, "Locale " + locale + " is not enabled.");
+ // TODO: Investigate what we should do for disabled locales.
+ return false;
}
-
+ final DictionaryFacilitator dictionaryFacilitator =
+ mDictionaryFacilitatorLruCache.get(locale);
if (DEBUG) {
Log.d(TAG, "testedWord: " + testedWord);
}
- final Boolean isCachedDistracter = mDistractersCache.get(testedWord);
+ final Pair<Locale, String> cacheKey = new Pair<>(locale, testedWord);
+ final Boolean isCachedDistracter = mDistractersCache.get(cacheKey);
if (isCachedDistracter != null && isCachedDistracter) {
if (DEBUG) {
Log.d(TAG, "isDistracter: true (cache hit)");
@@ -192,37 +189,38 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr
}
final boolean isDistracterCheckedByGetMaxFreqencyOfExactMatches =
- checkDistracterUsingMaxFreqencyOfExactMatches(testedWord);
+ checkDistracterUsingMaxFreqencyOfExactMatches(dictionaryFacilitator, testedWord);
if (isDistracterCheckedByGetMaxFreqencyOfExactMatches) {
- // Add the word to the cache.
- mDistractersCache.put(testedWord, Boolean.TRUE);
+ // Add the pair of locale and word to the cache.
+ mDistractersCache.put(cacheKey, Boolean.TRUE);
return true;
}
- final boolean isValidWord = mDictionaryFacilitator.isValidWord(testedWord,
- false /* ignoreCase */);
- if (isValidWord) {
- // Valid word is not a distractor.
+ final boolean Word = dictionaryFacilitator.isValidWord(testedWord, false /* ignoreCase */);
+ if (Word) {
+ // Valid word is not a distracter.
if (DEBUG) {
Log.d(TAG, "isDistracter: false (valid word)");
}
return false;
}
+ final Keyboard keyboard = getKeyboardForLocale(locale);
final boolean isDistracterCheckedByGetSuggestion =
- checkDistracterUsingGetSuggestions(testedWord);
+ checkDistracterUsingGetSuggestions(dictionaryFacilitator, keyboard, testedWord);
if (isDistracterCheckedByGetSuggestion) {
- // Add the word to the cache.
- mDistractersCache.put(testedWord, Boolean.TRUE);
+ // Add the pair of locale and word to the cache.
+ mDistractersCache.put(cacheKey, Boolean.TRUE);
return true;
}
return false;
}
- private boolean checkDistracterUsingMaxFreqencyOfExactMatches(final String testedWord) {
+ private static boolean checkDistracterUsingMaxFreqencyOfExactMatches(
+ final DictionaryFacilitator dictionaryFacilitator, final String testedWord) {
// The tested word is a distracter when there is a word that is exact matched to the tested
// word and its probability is higher than the tested word's probability.
- final int perfectMatchFreq = mDictionaryFacilitator.getFrequency(testedWord);
- final int exactMatchFreq = mDictionaryFacilitator.getMaxFrequencyOfExactMatches(testedWord);
+ final int perfectMatchFreq = dictionaryFacilitator.getFrequency(testedWord);
+ final int exactMatchFreq = dictionaryFacilitator.getMaxFrequencyOfExactMatches(testedWord);
final boolean isDistracter = perfectMatchFreq < exactMatchFreq;
if (DEBUG) {
Log.d(TAG, "perfectMatchFreq: " + perfectMatchFreq);
@@ -232,8 +230,10 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr
return isDistracter;
}
- private boolean checkDistracterUsingGetSuggestions(final String testedWord) {
- if (mKeyboard == null) {
+ private boolean checkDistracterUsingGetSuggestions(
+ final DictionaryFacilitator dictionaryFacilitator, final Keyboard keyboard,
+ final String testedWord) {
+ if (keyboard == null) {
return false;
}
final SettingsValuesForSuggestion settingsValuesForSuggestion =
@@ -246,24 +246,25 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr
testedWord;
final WordComposer composer = new WordComposer();
final int[] codePoints = StringUtils.toCodePointArray(testedWord);
-
+ final int[] coordinates = keyboard.getCoordinates(codePoints);
+ composer.setComposingWord(codePoints, coordinates);
+ final SuggestionResults suggestionResults;
synchronized (mLock) {
- final int[] coordinates = mKeyboard.getCoordinates(codePoints);
- composer.setComposingWord(codePoints, coordinates);
- final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
- composer, PrevWordsInfo.EMPTY_PREV_WORDS_INFO, mKeyboard.getProximityInfo(),
+ suggestionResults = dictionaryFacilitator.getSuggestionResults(composer,
+ NgramContext.EMPTY_PREV_WORDS_INFO,
+ keyboard.getProximityInfo().getNativeProximityInfo(),
settingsValuesForSuggestion, 0 /* sessionId */);
- if (suggestionResults.isEmpty()) {
- return false;
- }
- final SuggestedWordInfo firstSuggestion = suggestionResults.first();
- final boolean isDistractor = suggestionExceedsDistracterThreshold(
- firstSuggestion, consideredWord, DISTRACTER_WORD_SCORE_THRESHOLD);
- if (DEBUG) {
- Log.d(TAG, "isDistracter: " + isDistractor);
- }
- return isDistractor;
}
+ if (suggestionResults.isEmpty()) {
+ return false;
+ }
+ final SuggestedWordInfo firstSuggestion = suggestionResults.first();
+ final boolean isDistracter = suggestionExceedsDistracterThreshold(
+ firstSuggestion, consideredWord, DISTRACTER_WORD_SCORE_THRESHOLD);
+ if (DEBUG) {
+ Log.d(TAG, "isDistracter: " + isDistracter);
+ }
+ return isDistracter;
}
private static boolean suggestionExceedsDistracterThreshold(final SuggestedWordInfo suggestion,
@@ -283,4 +284,41 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr
}
return false;
}
+
+ private boolean shouldBeLowerCased(final NgramContext ngramContext, final String testedWord,
+ final Locale locale) {
+ final DictionaryFacilitator dictionaryFacilitator =
+ mDictionaryFacilitatorLruCache.get(locale);
+ if (dictionaryFacilitator.isValidWord(testedWord, false /* ignoreCase */)) {
+ return false;
+ }
+ final String lowerCaseTargetWord = testedWord.toLowerCase(locale);
+ if (testedWord.equals(lowerCaseTargetWord)) {
+ return false;
+ }
+ if (dictionaryFacilitator.isValidWord(lowerCaseTargetWord, false /* ignoreCase */)) {
+ return true;
+ }
+ if (StringUtils.getCapitalizationType(testedWord) == StringUtils.CAPITALIZE_FIRST
+ && !ngramContext.isValid()) {
+ // TODO: Check beginning-of-sentence.
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int getWordHandlingType(final NgramContext ngramContext, final String testedWord,
+ final Locale locale) {
+ // TODO: Use this method for user history dictionary.
+ if (testedWord == null|| locale == null) {
+ return HandlingType.getHandlingType(false /* shouldBeLowerCased */, false /* isOov */);
+ }
+ final boolean shouldBeLowerCased = shouldBeLowerCased(ngramContext, testedWord, locale);
+ final String caseModifiedWord =
+ shouldBeLowerCased ? testedWord.toLowerCase(locale) : testedWord;
+ final boolean isOov = !mDictionaryFacilitatorLruCache.get(locale).isValidWord(
+ caseModifiedWord, false /* ignoreCase */);
+ return HandlingType.getHandlingType(shouldBeLowerCased, isOov);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java
index 4ad4ba784..4c99fed9f 100644
--- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java
@@ -22,7 +22,7 @@ import java.util.Locale;
import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.PrevWordsInfo;
+import com.android.inputmethod.latin.NgramContext;
public class DistracterFilterCheckingIsInDictionary implements DistracterFilter {
private final DistracterFilter mDistracterFilter;
@@ -35,16 +35,21 @@ public class DistracterFilterCheckingIsInDictionary implements DistracterFilter
}
@Override
- public boolean isDistracterToWordsInDictionaries(PrevWordsInfo prevWordsInfo,
+ public boolean isDistracterToWordsInDictionaries(NgramContext ngramContext,
String testedWord, Locale locale) {
if (mDictionary.isInDictionary(testedWord)) {
// This filter treats entries that are already in the dictionary as non-distracters
// because they have passed the filtering in the past.
return false;
- } else {
- return mDistracterFilter.isDistracterToWordsInDictionaries(
- prevWordsInfo, testedWord, locale);
}
+ return mDistracterFilter.isDistracterToWordsInDictionaries(
+ ngramContext, testedWord, locale);
+ }
+
+ @Override
+ public int getWordHandlingType(final NgramContext ngramContext, final String testedWord,
+ final Locale locale) {
+ return mDistracterFilter.getWordHandlingType(ngramContext, testedWord, locale);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java
index 61da1b789..e77f6fd40 100644
--- a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java
@@ -27,7 +27,7 @@ import java.util.concurrent.ThreadFactory;
* Utilities to manage executors.
*/
public class ExecutorUtils {
- private static final ConcurrentHashMap<String, ExecutorService> sExecutorMap =
+ static final ConcurrentHashMap<String, ExecutorService> sExecutorMap =
new ConcurrentHashMap<>();
private static class ThreadFactoryWithId implements ThreadFactory {
@@ -49,7 +49,7 @@ public class ExecutorUtils {
public static ExecutorService getExecutor(final String id) {
ExecutorService executor = sExecutorMap.get(id);
if (executor == null) {
- synchronized(sExecutorMap) {
+ synchronized (sExecutorMap) {
executor = sExecutorMap.get(id);
if (executor == null) {
executor = Executors.newSingleThreadExecutor(new ThreadFactoryWithId(id));
@@ -65,7 +65,7 @@ public class ExecutorUtils {
*/
@UsedForTesting
public static void shutdownAllExecutors() {
- synchronized(sExecutorMap) {
+ synchronized (sExecutorMap) {
for (final ExecutorService executor : sExecutorMap.values()) {
executor.execute(new Runnable() {
@Override
diff --git a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java
index c2167a76b..ae2de44c7 100644
--- a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java
@@ -18,6 +18,7 @@ package com.android.inputmethod.latin.utils;
import com.android.inputmethod.dictionarypack.DictionarySettingsFragment;
import com.android.inputmethod.latin.about.AboutPreferences;
+import com.android.inputmethod.latin.settings.AccountsSettingsFragment;
import com.android.inputmethod.latin.settings.AdvancedSettingsFragment;
import com.android.inputmethod.latin.settings.AppearanceSettingsFragment;
import com.android.inputmethod.latin.settings.CorrectionSettingsFragment;
@@ -42,6 +43,7 @@ public class FragmentUtils {
sLatinImeFragments.add(DictionarySettingsFragment.class.getName());
sLatinImeFragments.add(AboutPreferences.class.getName());
sLatinImeFragments.add(PreferencesSettingsFragment.class.getName());
+ sLatinImeFragments.add(AccountsSettingsFragment.class.getName());
sLatinImeFragments.add(AppearanceSettingsFragment.class.getName());
sLatinImeFragments.add(ThemeSettingsFragment.class.getName());
sLatinImeFragments.add(MultiLingualSettingsFragment.class.getName());
diff --git a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
index ea406fa75..142548b25 100644
--- a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
@@ -53,7 +53,8 @@ public final class ImportantNoticeUtils {
// This utility class is not publicly instantiable.
}
- private static boolean isInSystemSetupWizard(final Context context) {
+ @UsedForTesting
+ static boolean isInSystemSetupWizard(final Context context) {
try {
final int userSetupComplete = Settings.Secure.getInt(
context.getContentResolver(), Settings_Secure_USER_SETUP_COMPLETE);
@@ -84,7 +85,8 @@ public final class ImportantNoticeUtils {
return getLastImportantNoticeVersion(context) + 1;
}
- private static boolean hasNewImportantNotice(final Context context) {
+ @UsedForTesting
+ static boolean hasNewImportantNotice(final Context context) {
final int lastVersion = getLastImportantNoticeVersion(context);
return getCurrentImportantNoticeVersion(context) > lastVersion;
}
diff --git a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
deleted file mode 100644
index fbce3f2fd..000000000
--- a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2014 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.utils;
-
-import android.util.Log;
-
-import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.DictionaryFacilitator;
-import com.android.inputmethod.latin.PrevWordsInfo;
-import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-
-// Note: this class is used as a parameter type of a native method. You should be careful when you
-// rename this class or field name. See BinaryDictionary#addMultipleDictionaryEntriesNative().
-public final class LanguageModelParam {
- private static final String TAG = LanguageModelParam.class.getSimpleName();
- private static final boolean DEBUG = false;
- private static final boolean DEBUG_TOKEN = false;
-
- // For now, these probability values are being referred to only when we add new entries to
- // decaying dynamic binary dictionaries. When these are referred to, what matters is 0 or
- // non-0. Thus, it's not meaningful to compare 10, 100, and so on.
- // TODO: Revise the logic in ForgettingCurveUtils in native code.
- private static final int UNIGRAM_PROBABILITY_FOR_VALID_WORD = 100;
- private static final int UNIGRAM_PROBABILITY_FOR_OOV_WORD = Dictionary.NOT_A_PROBABILITY;
- private static final int BIGRAM_PROBABILITY_FOR_VALID_WORD = 10;
- private static final int BIGRAM_PROBABILITY_FOR_OOV_WORD = Dictionary.NOT_A_PROBABILITY;
-
- public final CharSequence mTargetWord;
- public final int[] mWord0;
- public final int[] mWord1;
- // TODO: this needs to be a list of shortcuts
- public final int[] mShortcutTarget;
- public final int mUnigramProbability;
- public final int mBigramProbability;
- public final int mShortcutProbability;
- public final boolean mIsNotAWord;
- public final boolean mIsBlacklisted;
- // Time stamp in seconds.
- public final int mTimestamp;
-
- // Constructor for unigram. TODO: support shortcuts
- public LanguageModelParam(final CharSequence word, final int unigramProbability,
- final int timestamp) {
- this(null /* word0 */, word, unigramProbability, Dictionary.NOT_A_PROBABILITY, timestamp);
- }
-
- // Constructor for unigram and bigram.
- public LanguageModelParam(final CharSequence word0, final CharSequence word1,
- final int unigramProbability, final int bigramProbability,
- final int timestamp) {
- mTargetWord = word1;
- mWord0 = (word0 == null) ? null : StringUtils.toCodePointArray(word0);
- mWord1 = StringUtils.toCodePointArray(word1);
- mShortcutTarget = null;
- mUnigramProbability = unigramProbability;
- mBigramProbability = bigramProbability;
- mShortcutProbability = Dictionary.NOT_A_PROBABILITY;
- mIsNotAWord = false;
- mIsBlacklisted = false;
- mTimestamp = timestamp;
- }
-
- // Process a list of words and return a list of {@link LanguageModelParam} objects.
- public static ArrayList<LanguageModelParam> createLanguageModelParamsFrom(
- final List<String> tokens, final int timestamp,
- final DictionaryFacilitator dictionaryFacilitator,
- final SpacingAndPunctuations spacingAndPunctuations,
- final DistracterFilter distracterFilter) {
- final ArrayList<LanguageModelParam> languageModelParams = new ArrayList<>();
- final int N = tokens.size();
- PrevWordsInfo prevWordsInfo = PrevWordsInfo.EMPTY_PREV_WORDS_INFO;
- for (int i = 0; i < N; ++i) {
- final String tempWord = tokens.get(i);
- if (StringUtils.isEmptyStringOrWhiteSpaces(tempWord)) {
- // just skip this token
- if (DEBUG_TOKEN) {
- Log.d(TAG, "--- isEmptyStringOrWhiteSpaces: \"" + tempWord + "\"");
- }
- continue;
- }
- if (!DictionaryInfoUtils.looksValidForDictionaryInsertion(
- tempWord, spacingAndPunctuations)) {
- if (DEBUG_TOKEN) {
- Log.d(TAG, "--- not looksValidForDictionaryInsertion: \""
- + tempWord + "\"");
- }
- // Sentence terminator found. Split.
- prevWordsInfo = PrevWordsInfo.EMPTY_PREV_WORDS_INFO;
- continue;
- }
- if (DEBUG_TOKEN) {
- Log.d(TAG, "--- word: \"" + tempWord + "\"");
- }
- final LanguageModelParam languageModelParam =
- detectWhetherVaildWordOrNotAndGetLanguageModelParam(
- prevWordsInfo, tempWord, timestamp, dictionaryFacilitator,
- distracterFilter);
- if (languageModelParam == null) {
- continue;
- }
- languageModelParams.add(languageModelParam);
- prevWordsInfo = prevWordsInfo.getNextPrevWordsInfo(
- new PrevWordsInfo.WordInfo(tempWord));
- }
- return languageModelParams;
- }
-
- private static LanguageModelParam detectWhetherVaildWordOrNotAndGetLanguageModelParam(
- final PrevWordsInfo prevWordsInfo, final String targetWord, final int timestamp,
- final DictionaryFacilitator dictionaryFacilitator,
- final DistracterFilter distracterFilter) {
- final Locale locale = dictionaryFacilitator.getLocale();
- if (locale == null) {
- return null;
- }
- if (dictionaryFacilitator.isValidWord(targetWord, false /* ignoreCase */)) {
- return createAndGetLanguageModelParamOfWord(prevWordsInfo, targetWord, timestamp,
- true /* isValidWord */, locale, distracterFilter);
- }
-
- final String lowerCaseTargetWord = targetWord.toLowerCase(locale);
- if (dictionaryFacilitator.isValidWord(lowerCaseTargetWord, false /* ignoreCase */)) {
- // Add the lower-cased word.
- return createAndGetLanguageModelParamOfWord(prevWordsInfo, lowerCaseTargetWord,
- timestamp, true /* isValidWord */, locale, distracterFilter);
- }
-
- // Treat the word as an OOV word.
- return createAndGetLanguageModelParamOfWord(prevWordsInfo, targetWord, timestamp,
- false /* isValidWord */, locale, distracterFilter);
- }
-
- private static LanguageModelParam createAndGetLanguageModelParamOfWord(
- final PrevWordsInfo prevWordsInfo, final String targetWord, final int timestamp,
- final boolean isValidWord, final Locale locale,
- final DistracterFilter distracterFilter) {
- final String word;
- if (StringUtils.getCapitalizationType(targetWord) == StringUtils.CAPITALIZE_FIRST
- && !prevWordsInfo.isValid() && !isValidWord) {
- word = targetWord.toLowerCase(locale);
- } else {
- word = targetWord;
- }
- // Check whether the word is a distracter to words in the dictionaries.
- if (distracterFilter.isDistracterToWordsInDictionaries(prevWordsInfo, word, locale)) {
- if (DEBUG) {
- Log.d(TAG, "The word (" + word + ") is a distracter. Skip this word.");
- }
- return null;
- }
- final int unigramProbability = isValidWord ?
- UNIGRAM_PROBABILITY_FOR_VALID_WORD : UNIGRAM_PROBABILITY_FOR_OOV_WORD;
- if (!prevWordsInfo.isValid()) {
- if (DEBUG) {
- Log.d(TAG, "--- add unigram: current("
- + (isValidWord ? "Valid" : "OOV") + ") = " + word);
- }
- return new LanguageModelParam(word, unigramProbability, timestamp);
- }
- if (DEBUG) {
- Log.d(TAG, "--- add bigram: prev = " + prevWordsInfo + ", current("
- + (isValidWord ? "Valid" : "OOV") + ") = " + word);
- }
- final int bigramProbability = isValidWord ?
- BIGRAM_PROBABILITY_FOR_VALID_WORD : BIGRAM_PROBABILITY_FOR_OOV_WORD;
- return new LanguageModelParam(prevWordsInfo.mPrevWordsInfo[0].mWord, word,
- unigramProbability, bigramProbability, timestamp);
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java b/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java
index dd6fac671..9a5be99b3 100644
--- a/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java
+++ b/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java
@@ -21,21 +21,22 @@ import android.os.Looper;
import java.lang.ref.WeakReference;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
public class LeakGuardHandlerWrapper<T> extends Handler {
private final WeakReference<T> mOwnerInstanceRef;
- public LeakGuardHandlerWrapper(final T ownerInstance) {
+ public LeakGuardHandlerWrapper(@Nonnull final T ownerInstance) {
this(ownerInstance, Looper.myLooper());
}
- public LeakGuardHandlerWrapper(final T ownerInstance, final Looper looper) {
+ public LeakGuardHandlerWrapper(@Nonnull final T ownerInstance, final Looper looper) {
super(looper);
- if (ownerInstance == null) {
- throw new NullPointerException("ownerInstance is null");
- }
mOwnerInstanceRef = new WeakReference<>(ownerInstance);
}
+ @Nullable
public T getOwnerInstance() {
return mOwnerInstanceRef.get();
}
diff --git a/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/NgramContextUtils.java
index 3cd63612c..7d2ddd268 100644
--- a/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/NgramContextUtils.java
@@ -16,15 +16,18 @@
package com.android.inputmethod.latin.utils;
+import com.android.inputmethod.latin.NgramContext;
+import com.android.inputmethod.latin.NgramContext.WordInfo;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
+
+import java.util.Arrays;
import java.util.regex.Pattern;
-import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.PrevWordsInfo;
-import com.android.inputmethod.latin.PrevWordsInfo.WordInfo;
-import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
+import javax.annotation.Nonnull;
-public final class PrevWordsInfoUtils {
- private PrevWordsInfoUtils() {
+public final class NgramContextUtils {
+ private NgramContextUtils() {
// Intentional empty constructor for utility class.
}
@@ -43,7 +46,7 @@ public final class PrevWordsInfoUtils {
// (n = 2) "abc def|" -> beginning-of-sentence, abc
// (n = 2) "abc def |" -> beginning-of-sentence, abc
// (n = 2) "abc 'def|" -> empty. The context is different from "abc def", but we cannot
- // represent this situation using PrevWordsInfo. See TODO in the method.
+ // represent this situation using NgramContext. See TODO in the method.
// TODO: The next example's result should be "abc, def". This have to be fixed before we
// retrieve the prior context of Beginning-of-Sentence.
// (n = 2) "abc def. |" -> beginning-of-sentence, abc
@@ -51,11 +54,13 @@ public final class PrevWordsInfoUtils {
// (n = 2) "abc|" -> beginning-of-sentence
// (n = 2) "abc |" -> beginning-of-sentence
// (n = 2) "abc. def|" -> beginning-of-sentence
- public static PrevWordsInfo getPrevWordsInfoFromNthPreviousWord(final CharSequence prev,
+ @Nonnull
+ public static NgramContext getNgramContextFromNthPreviousWord(final CharSequence prev,
final SpacingAndPunctuations spacingAndPunctuations, final int n) {
- if (prev == null) return PrevWordsInfo.EMPTY_PREV_WORDS_INFO;
+ if (prev == null) return NgramContext.EMPTY_PREV_WORDS_INFO;
final String[] w = SPACE_REGEX.split(prev);
final WordInfo[] prevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
+ Arrays.fill(prevWordsInfo, WordInfo.EMPTY_WORD_INFO);
for (int i = 0; i < prevWordsInfo.length; i++) {
final int focusedWordIndex = w.length - n - i;
// Referring to the word after the focused word.
@@ -66,38 +71,36 @@ public final class PrevWordsInfoUtils {
if (spacingAndPunctuations.isWordConnector(firstChar)) {
// The word following the focused word is starting with a word connector.
// TODO: Return meaningful context for this case.
- prevWordsInfo[i] = WordInfo.EMPTY_WORD_INFO;
break;
}
}
}
// If we can't find (n + i) words, the context is beginning-of-sentence.
if (focusedWordIndex < 0) {
- prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE;
+ prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO;
break;
}
final String focusedWord = w[focusedWordIndex];
// If the word is, the context is beginning-of-sentence.
final int length = focusedWord.length();
if (length <= 0) {
- prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE;
+ prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO;
break;
}
// If ends in a sentence separator, the context is beginning-of-sentence.
final char lastChar = focusedWord.charAt(length - 1);
if (spacingAndPunctuations.isSentenceSeparator(lastChar)) {
- prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE;
+ prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO;
break;
}
// If ends in a word separator or connector, the context is unclear.
// TODO: Return meaningful context for this case.
if (spacingAndPunctuations.isWordSeparator(lastChar)
|| spacingAndPunctuations.isWordConnector(lastChar)) {
- prevWordsInfo[i] = WordInfo.EMPTY_WORD_INFO;
break;
}
prevWordsInfo[i] = new WordInfo(focusedWord);
}
- return new PrevWordsInfo(prevWordsInfo);
+ return new NgramContext(prevWordsInfo);
}
}
diff --git a/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
index e3cac97f0..a381649a4 100644
--- a/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
+++ b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
@@ -16,6 +16,8 @@
package com.android.inputmethod.latin.utils;
+import com.android.inputmethod.latin.common.StringUtils;
+
import java.util.Locale;
/**
@@ -49,6 +51,17 @@ public class RecapitalizeStatus {
}
}
+ public static String modeToString(final int recapitalizeMode) {
+ switch (recapitalizeMode) {
+ case NOT_A_RECAPITALIZE_MODE: return "undefined";
+ case CAPS_MODE_ORIGINAL_MIXED_CASE: return "mixedCase";
+ case CAPS_MODE_ALL_LOWER: return "allLower";
+ case CAPS_MODE_FIRST_WORD_UPPER: return "firstWordUpper";
+ case CAPS_MODE_ALL_UPPER: return "allUpper";
+ default: return "unknown<" + recapitalizeMode + ">";
+ }
+ }
+
/**
* We store the location of the cursor and the string that was there before the recapitalize
* action was done, and the location of the cursor and the string that was there after.
diff --git a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
deleted file mode 100644
index 64c9e2cff..000000000
--- a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
+++ /dev/null
@@ -1,155 +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.utils;
-
-import java.util.Arrays;
-
-// TODO: This class is not thread-safe.
-public final 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 addAt(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;
- }
- }
-
- /**
- * Shift to the left by elementCount, discarding elementCount pointers at the start.
- * @param elementCount how many elements to shift.
- */
- public void shift(final int elementCount) {
- System.arraycopy(mArray, elementCount, mArray, 0, mLength - elementCount);
- mLength -= elementCount;
- }
-
- @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/utils/ResourceUtils.java b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
index 093c5a6c1..cc0d470df 100644
--- a/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
@@ -26,6 +26,7 @@ import android.util.TypedValue;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.settings.SettingsValues;
import java.util.ArrayList;
import java.util.HashMap;
@@ -110,7 +111,6 @@ public final class ResourceUtils {
* are true for the specified key value pairs.
*
* For example, "condition,constant" has the following format.
- * (See {@link ResourceUtilsTests#testFindConstantForKeyValuePairsRegexp()})
* - HARDWARE=mako,constantForNexus4
* - MODEL=Nexus 4:MANUFACTURER=LGE,constantForNexus4
* - ,defaultConstant
@@ -119,6 +119,7 @@ public final class ResourceUtils {
* @param conditionConstantArray an array of "condition,constant" elements to be searched.
* @return the constant part of the matched "condition,constant" element. Returns null if no
* condition matches.
+ * @see com.android.inputmethod.latin.utils.ResourceUtilsTests#testFindConstantForKeyValuePairsRegexp()
*/
@UsedForTesting
static String findConstantForKeyValuePairs(final HashMap<String, String> keyValuePairs,
@@ -186,6 +187,15 @@ public final class ResourceUtils {
return dm.widthPixels;
}
+ public static int getKeyboardHeight(final Resources res, final SettingsValues settingsValues) {
+ final int defaultKeyboardHeight = getDefaultKeyboardHeight(res);
+ if (settingsValues.mHasKeyboardResize) {
+ // mKeyboardHeightScale Ranges from [.5,1.2], from xml/prefs_screen_debug.xml
+ return (int)(defaultKeyboardHeight * settingsValues.mKeyboardHeightScale);
+ }
+ return defaultKeyboardHeight;
+ }
+
public static int getDefaultKeyboardHeight(final Resources res) {
final DisplayMetrics dm = res.getDisplayMetrics();
final String keyboardHeightInDp = getDeviceOverrideValue(
diff --git a/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java b/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java
deleted file mode 100644
index 1ca895fdb..000000000
--- a/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2014 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.utils;
-
-import android.view.inputmethod.InputMethodSubtype;
-
-public final class SpacebarLanguageUtils {
- private SpacebarLanguageUtils() {
- // Intentional empty constructor for utility class.
- }
-
- // InputMethodSubtype's display name for spacebar text in its locale.
- // isAdditionalSubtype (T=true, F=false)
- // locale layout | Middle Full
- // ------ ------- - --------- ----------------------
- // en_US qwerty F English English (US) exception
- // en_GB qwerty F English English (UK) exception
- // es_US spanish F Español Español (EE.UU.) exception
- // fr azerty F Français Français
- // fr_CA qwerty F Français Français (Canada)
- // fr_CH swiss F Français Français (Suisse)
- // de qwertz F Deutsch Deutsch
- // de_CH swiss T Deutsch Deutsch (Schweiz)
- // zz qwerty F QWERTY QWERTY
- // fr qwertz T Français Français
- // de qwerty T Deutsch Deutsch
- // en_US azerty T English English (US)
- // zz azerty T AZERTY AZERTY
- // Get InputMethodSubtype's full display name in its locale.
- public static String getFullDisplayName(final InputMethodSubtype subtype) {
- if (SubtypeLocaleUtils.isNoLanguage(subtype)) {
- return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype);
- }
- return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(subtype.getLocale());
- }
-
- // Get InputMethodSubtype's middle display name in its locale.
- public static String getMiddleDisplayName(final InputMethodSubtype subtype) {
- if (SubtypeLocaleUtils.isNoLanguage(subtype)) {
- return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype);
- }
- return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(subtype.getLocale());
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java
index 38164cb36..c41817fe6 100644
--- a/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java
@@ -24,6 +24,12 @@ import android.text.TextUtils;
import android.text.style.SuggestionSpan;
import android.text.style.URLSpan;
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
public final class SpannableStringUtils {
/**
* Copies the spans from the region <code>start...end</code> in
@@ -51,7 +57,7 @@ public final class SpannableStringUtils {
// of a word. But the spans have been split into two by the getText{Before,After}Cursor
// methods, so after concatenation they may end in the middle of a word.
// Since we don't use them, we can just remove them and avoid crashing.
- fl &= ~Spannable.SPAN_PARAGRAPH;
+ fl &= ~Spanned.SPAN_PARAGRAPH;
int st = source.getSpanStart(spans[i]);
int en = source.getSpanEnd(spans[i]);
@@ -125,4 +131,53 @@ public final class SpannableStringUtils {
final URLSpan[] spans = spanned.getSpans(startIndex - 1, endIndex + 1, URLSpan.class);
return null != spans && spans.length > 0;
}
+
+ /**
+ * Splits the given {@code charSequence} with at occurrences of the given {@code regex}.
+ * <p>
+ * This is equivalent to
+ * {@code charSequence.toString().split(regex, preserveTrailingEmptySegments ? -1 : 0)}
+ * except that the spans are preserved in the result array.
+ * </p>
+ * @param charSequence the character sequence to be split.
+ * @param regex the regex pattern to be used as the separator.
+ * @param preserveTrailingEmptySegments {@code true} to preserve the trailing empty
+ * segments. Otherwise, trailing empty segments will be removed before being returned.
+ * @return the array which contains the result. All the spans in the <code>charSequence</code>
+ * is preserved.
+ */
+ @UsedForTesting
+ public static CharSequence[] split(final CharSequence charSequence, final String regex,
+ final boolean preserveTrailingEmptySegments) {
+ // A short-cut for non-spanned strings.
+ if (!(charSequence instanceof Spanned)) {
+ // -1 means that trailing empty segments will be preserved.
+ return charSequence.toString().split(regex, preserveTrailingEmptySegments ? -1 : 0);
+ }
+
+ // Hereafter, emulate String.split for CharSequence.
+ final ArrayList<CharSequence> sequences = new ArrayList<>();
+ final Matcher matcher = Pattern.compile(regex).matcher(charSequence);
+ int nextStart = 0;
+ boolean matched = false;
+ while (matcher.find()) {
+ sequences.add(charSequence.subSequence(nextStart, matcher.start()));
+ nextStart = matcher.end();
+ matched = true;
+ }
+ if (!matched) {
+ // never matched. preserveTrailingEmptySegments is ignored in this case.
+ return new CharSequence[] { charSequence };
+ }
+ sequences.add(charSequence.subSequence(nextStart, charSequence.length()));
+ if (!preserveTrailingEmptySegments) {
+ for (int i = sequences.size() - 1; i >= 0; --i) {
+ if (!TextUtils.isEmpty(sequences.get(i))) {
+ break;
+ }
+ sequences.remove(i);
+ }
+ }
+ return sequences.toArray(new CharSequence[sequences.size()]);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
deleted file mode 100644
index 1781924ac..000000000
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ /dev/null
@@ -1,631 +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.utils;
-
-import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED;
-
-import android.text.Spanned;
-import android.text.TextUtils;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.Constants;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public final class StringUtils {
- 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 static final String EMPTY_STRING = "";
-
- private static final char CHAR_LINE_FEED = 0X000A;
- private static final char CHAR_VERTICAL_TAB = 0X000B;
- private static final char CHAR_FORM_FEED = 0X000C;
- private static final char CHAR_CARRIAGE_RETURN = 0X000D;
- private static final char CHAR_NEXT_LINE = 0X0085;
- private static final char CHAR_LINE_SEPARATOR = 0X2028;
- private static final char CHAR_PARAGRAPH_SEPARATOR = 0X2029;
-
- private StringUtils() {
- // This utility class is not publicly instantiable.
- }
-
- public static int codePointCount(final String text) {
- if (TextUtils.isEmpty(text)) return 0;
- return text.codePointCount(0, text.length());
- }
-
- public static String newSingleCodePointString(int codePoint) {
- if (Character.charCount(codePoint) == 1) {
- // Optimization: avoid creating a temporary array for characters that are
- // represented by a single char value
- return String.valueOf((char) codePoint);
- }
- // For surrogate pair
- return new String(Character.toChars(codePoint));
- }
-
- public static boolean containsInArray(final String text, final String[] array) {
- for (final String element : array) {
- if (text.equals(element)) return true;
- }
- return false;
- }
-
- /**
- * Comma-Splittable Text is similar to Comma-Separated Values (CSV) but has much simpler syntax.
- * Unlike CSV, Comma-Splittable Text has no escaping mechanism, so that the text can't contain
- * a comma character in it.
- */
- private static final String SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT = ",";
-
- public static boolean containsInCommaSplittableText(final String text,
- final String extraValues) {
- if (TextUtils.isEmpty(extraValues)) {
- return false;
- }
- return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT));
- }
-
- public static String removeFromCommaSplittableTextIfExists(final String text,
- final String extraValues) {
- if (TextUtils.isEmpty(extraValues)) {
- return EMPTY_STRING;
- }
- final String[] elements = extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT);
- if (!containsInArray(text, elements)) {
- return extraValues;
- }
- final ArrayList<String> result = new ArrayList<>(elements.length - 1);
- for (final String element : elements) {
- if (!text.equals(element)) {
- result.add(element);
- }
- }
- return TextUtils.join(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT, result);
- }
-
- /**
- * Remove duplicates from an array of strings.
- *
- * This method will always keep the first occurrence of all strings at their position
- * in the array, removing the subsequent ones.
- */
- public static void removeDupes(final ArrayList<String> suggestions) {
- if (suggestions.size() < 2) return;
- int i = 1;
- // Don't cache suggestions.size(), since we may be removing items
- while (i < suggestions.size()) {
- final String cur = suggestions.get(i);
- // Compare each suggestion with each previous suggestion
- for (int j = 0; j < i; j++) {
- final String previous = suggestions.get(j);
- if (TextUtils.equals(cur, previous)) {
- suggestions.remove(i);
- i--;
- break;
- }
- }
- i++;
- }
- }
-
- public static String capitalizeFirstCodePoint(final String s, final Locale locale) {
- if (s.length() <= 1) {
- return s.toUpperCase(locale);
- }
- // Please refer to the comment below in
- // {@link #capitalizeFirstAndDowncaseRest(String,Locale)} as this has the same shortcomings
- final int cutoff = s.offsetByCodePoints(0, 1);
- return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff);
- }
-
- public static String capitalizeFirstAndDowncaseRest(final String s, final Locale locale) {
- if (s.length() <= 1) {
- return s.toUpperCase(locale);
- }
- // TODO: fix the bugs below
- // - This does not work for Greek, because it returns upper case instead of title case.
- // - It does not work for Serbian, because it fails to account for the "lj" character,
- // which should be "Lj" in title case and "LJ" in upper case.
- // - It does not work for Dutch, because it fails to account for the "ij" digraph when it's
- // written as two separate code points. They are two different characters but both should
- // be capitalized as "IJ" as if they were a single letter in most words (not all). If the
- // unicode char for the ligature is used however, it works.
- final int cutoff = s.offsetByCodePoints(0, 1);
- return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff).toLowerCase(locale);
- }
-
- private static final int[] EMPTY_CODEPOINTS = {};
-
- public static int[] toCodePointArray(final CharSequence charSequence) {
- return toCodePointArray(charSequence, 0, charSequence.length());
- }
-
- /**
- * Converts a range of a string to an array of code points.
- * @param charSequence the source string.
- * @param startIndex the start index inside the string in java chars, inclusive.
- * @param endIndex the end index inside the string in java chars, exclusive.
- * @return a new array of code points. At most endIndex - startIndex, but possibly less.
- */
- public static int[] toCodePointArray(final CharSequence charSequence,
- final int startIndex, final int endIndex) {
- final int length = charSequence.length();
- if (length <= 0) {
- return EMPTY_CODEPOINTS;
- }
- final int[] codePoints =
- new int[Character.codePointCount(charSequence, startIndex, endIndex)];
- copyCodePointsAndReturnCodePointCount(codePoints, charSequence, startIndex, endIndex,
- false /* downCase */);
- return codePoints;
- }
-
- /**
- * Copies the codepoints in a CharSequence to an int array.
- *
- * This method assumes there is enough space in the array to store the code points. The size
- * can be measured with Character#codePointCount(CharSequence, int, int) before passing to this
- * method. If the int array is too small, an ArrayIndexOutOfBoundsException will be thrown.
- * Also, this method makes no effort to be thread-safe. Do not modify the CharSequence while
- * this method is running, or the behavior is undefined.
- * This method can optionally downcase code points before copying them, but it pays no attention
- * to locale while doing so.
- *
- * @param destination the int array.
- * @param charSequence the CharSequence.
- * @param startIndex the start index inside the string in java chars, inclusive.
- * @param endIndex the end index inside the string in java chars, exclusive.
- * @param downCase if this is true, code points will be downcased before being copied.
- * @return the number of copied code points.
- */
- public static int copyCodePointsAndReturnCodePointCount(final int[] destination,
- final CharSequence charSequence, final int startIndex, final int endIndex,
- final boolean downCase) {
- int destIndex = 0;
- for (int index = startIndex; index < endIndex;
- index = Character.offsetByCodePoints(charSequence, index, 1)) {
- final int codePoint = Character.codePointAt(charSequence, index);
- // TODO: stop using this, as it's not aware of the locale and does not always do
- // the right thing.
- destination[destIndex] = downCase ? Character.toLowerCase(codePoint) : codePoint;
- destIndex++;
- }
- return destIndex;
- }
-
- public static int[] toSortedCodePointArray(final String string) {
- final int[] codePoints = toCodePointArray(string);
- Arrays.sort(codePoints);
- return codePoints;
- }
-
- /**
- * Construct a String from a code point array
- *
- * @param codePoints a code point array that is null terminated when its logical length is
- * shorter than the array length.
- * @return a string constructed from the code point array.
- */
- public static String getStringFromNullTerminatedCodePointArray(final int[] codePoints) {
- int stringLength = codePoints.length;
- for (int i = 0; i < codePoints.length; i++) {
- if (codePoints[i] == 0) {
- stringLength = i;
- break;
- }
- }
- return new String(codePoints, 0 /* offset */, stringLength);
- }
-
- // This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE.
- public static int getCapitalizationType(final String text) {
- // If the first char is not uppercase, then the word is either all lower case or
- // camel case, and in either case we return CAPITALIZE_NONE.
- final int len = text.length();
- int index = 0;
- for (; index < len; index = text.offsetByCodePoints(index, 1)) {
- if (Character.isLetter(text.codePointAt(index))) {
- break;
- }
- }
- if (index == len) return CAPITALIZE_NONE;
- if (!Character.isUpperCase(text.codePointAt(index))) {
- return CAPITALIZE_NONE;
- }
- int capsCount = 1;
- int letterCount = 1;
- for (index = text.offsetByCodePoints(index, 1); index < len;
- index = text.offsetByCodePoints(index, 1)) {
- if (1 != capsCount && letterCount != capsCount) break;
- final int codePoint = text.codePointAt(index);
- if (Character.isUpperCase(codePoint)) {
- ++capsCount;
- ++letterCount;
- } else if (Character.isLetter(codePoint)) {
- // We need to discount non-letters since they may not be upper-case, but may
- // still be part of a word (e.g. single quote or dash, as in "IT'S" or "FULL-TIME")
- ++letterCount;
- }
- }
- // We know the first char is upper case. So we want to test if either every letter other
- // than the first is lower case, or if they are all upper case. If the string is exactly
- // one char long, then we will arrive here with letterCount 1, and this is correct, too.
- if (1 == capsCount) return CAPITALIZE_FIRST;
- return (letterCount == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE);
- }
-
- public static boolean isIdenticalAfterUpcase(final String text) {
- final int length = text.length();
- int i = 0;
- while (i < length) {
- final int codePoint = text.codePointAt(i);
- if (Character.isLetter(codePoint) && !Character.isUpperCase(codePoint)) {
- return false;
- }
- i += Character.charCount(codePoint);
- }
- return true;
- }
-
- public static boolean isIdenticalAfterDowncase(final String text) {
- final int length = text.length();
- int i = 0;
- while (i < length) {
- final int codePoint = text.codePointAt(i);
- if (Character.isLetter(codePoint) && !Character.isLowerCase(codePoint)) {
- return false;
- }
- i += Character.charCount(codePoint);
- }
- return true;
- }
-
- public static boolean isIdenticalAfterCapitalizeEachWord(final String text,
- final int[] sortedSeparators) {
- boolean needsCapsNext = true;
- final int len = text.length();
- for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
- final int codePoint = text.codePointAt(i);
- if (Character.isLetter(codePoint)) {
- if ((needsCapsNext && !Character.isUpperCase(codePoint))
- || (!needsCapsNext && !Character.isLowerCase(codePoint))) {
- return false;
- }
- }
- // We need a capital letter next if this is a separator.
- needsCapsNext = (Arrays.binarySearch(sortedSeparators, codePoint) >= 0);
- }
- return true;
- }
-
- // TODO: like capitalizeFirst*, this does not work perfectly for Dutch because of the IJ digraph
- // which should be capitalized together in *some* cases.
- public static String capitalizeEachWord(final String text, final int[] sortedSeparators,
- final Locale locale) {
- final StringBuilder builder = new StringBuilder();
- boolean needsCapsNext = true;
- final int len = text.length();
- for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
- final String nextChar = text.substring(i, text.offsetByCodePoints(i, 1));
- if (needsCapsNext) {
- builder.append(nextChar.toUpperCase(locale));
- } else {
- builder.append(nextChar.toLowerCase(locale));
- }
- // We need a capital letter next if this is a separator.
- needsCapsNext = (Arrays.binarySearch(sortedSeparators, nextChar.codePointAt(0)) >= 0);
- }
- return builder.toString();
- }
-
- /**
- * Approximates whether the text before the cursor looks like a URL.
- *
- * This is not foolproof, but it should work well in the practice.
- * Essentially it walks backward from the cursor until it finds something that's not a letter,
- * digit, or common URL symbol like underscore. If it hasn't found a period yet, then it
- * does not look like a URL.
- * If the text:
- * - starts with www and contains a period
- * - starts with a slash preceded by either a slash, whitespace, or start-of-string
- * Then it looks like a URL and we return true. Otherwise, we return false.
- *
- * Note: this method is called quite often, and should be fast.
- *
- * TODO: This will return that "abc./def" and ".abc/def" look like URLs to keep down the
- * code complexity, but ideally it should not. It's acceptable for now.
- */
- public static boolean lastPartLooksLikeURL(final CharSequence text) {
- int i = text.length();
- if (0 == i) return false;
- int wCount = 0;
- int slashCount = 0;
- boolean hasSlash = false;
- boolean hasPeriod = false;
- int codePoint = 0;
- while (i > 0) {
- codePoint = Character.codePointBefore(text, i);
- if (codePoint < Constants.CODE_PERIOD || codePoint > 'z') {
- // Handwavy heuristic to see if that's a URL character. Anything between period
- // and z. This includes all lower- and upper-case ascii letters, period,
- // underscore, arrobase, question mark, equal sign. It excludes spaces, exclamation
- // marks, double quotes...
- // Anything that's not a URL-like character causes us to break from here and
- // evaluate normally.
- break;
- }
- if (Constants.CODE_PERIOD == codePoint) {
- hasPeriod = true;
- }
- if (Constants.CODE_SLASH == codePoint) {
- hasSlash = true;
- if (2 == ++slashCount) {
- return true;
- }
- } else {
- slashCount = 0;
- }
- if ('w' == codePoint) {
- ++wCount;
- } else {
- wCount = 0;
- }
- i = Character.offsetByCodePoints(text, i, -1);
- }
- // End of the text run.
- // If it starts with www and includes a period, then it looks like a URL.
- if (wCount >= 3 && hasPeriod) return true;
- // If it starts with a slash, and the code point before is whitespace, it looks like an URL.
- if (1 == slashCount && (0 == i || Character.isWhitespace(codePoint))) return true;
- // If it has both a period and a slash, it looks like an URL.
- if (hasPeriod && hasSlash) return true;
- // Otherwise, it doesn't look like an URL.
- return false;
- }
-
- /**
- * Examines the string and returns whether we're inside a double quote.
- *
- * This is used to decide whether we should put an automatic space before or after a double
- * quote character. If we're inside a quotation, then we want to close it, so we want a space
- * after and not before. Otherwise, we want to open the quotation, so we want a space before
- * and not after. Exception: after a digit, we never want a space because the "inch" or
- * "minutes" use cases is dominant after digits.
- * In the practice, we determine whether we are in a quotation or not by finding the previous
- * double quote character, and looking at whether it's followed by whitespace. If so, that
- * was a closing quotation mark, so we're not inside a double quote. If it's not followed
- * by whitespace, then it was an opening quotation mark, and we're inside a quotation.
- *
- * @param text the text to examine.
- * @return whether we're inside a double quote.
- */
- public static boolean isInsideDoubleQuoteOrAfterDigit(final CharSequence text) {
- int i = text.length();
- if (0 == i) return false;
- int codePoint = Character.codePointBefore(text, i);
- if (Character.isDigit(codePoint)) return true;
- int prevCodePoint = 0;
- while (i > 0) {
- codePoint = Character.codePointBefore(text, i);
- if (Constants.CODE_DOUBLE_QUOTE == codePoint) {
- // If we see a double quote followed by whitespace, then that
- // was a closing quote.
- if (Character.isWhitespace(prevCodePoint)) return false;
- }
- if (Character.isWhitespace(codePoint) && Constants.CODE_DOUBLE_QUOTE == prevCodePoint) {
- // If we see a double quote preceded by whitespace, then that
- // was an opening quote. No need to continue seeking.
- return true;
- }
- i -= Character.charCount(codePoint);
- prevCodePoint = codePoint;
- }
- // We reached the start of text. If the first char is a double quote, then we're inside
- // a double quote. Otherwise we're not.
- return Constants.CODE_DOUBLE_QUOTE == codePoint;
- }
-
- public static boolean isEmptyStringOrWhiteSpaces(final String s) {
- final int N = codePointCount(s);
- for (int i = 0; i < N; ++i) {
- if (!Character.isWhitespace(s.codePointAt(i))) {
- return false;
- }
- }
- return true;
- }
-
- @UsedForTesting
- public static String byteArrayToHexString(final byte[] bytes) {
- if (bytes == null || bytes.length == 0) {
- return EMPTY_STRING;
- }
- final StringBuilder sb = new StringBuilder();
- for (byte b : bytes) {
- sb.append(String.format("%02x", b & 0xff));
- }
- return sb.toString();
- }
-
- /**
- * Convert hex string to byte array. The string length must be an even number.
- */
- @UsedForTesting
- public static byte[] hexStringToByteArray(final String hexString) {
- if (TextUtils.isEmpty(hexString)) {
- return null;
- }
- final int N = hexString.length();
- if (N % 2 != 0) {
- throw new NumberFormatException("Input hex string length must be an even number."
- + " Length = " + N);
- }
- final byte[] bytes = new byte[N / 2];
- for (int i = 0; i < N; i += 2) {
- bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
- + Character.digit(hexString.charAt(i + 1), 16));
- }
- return bytes;
- }
-
- public static String toUpperCaseOfStringForLocale(final String text,
- final boolean needsToUpperCase, final Locale locale) {
- if (text == null || !needsToUpperCase) return text;
- return text.toUpperCase(locale);
- }
-
- public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase,
- final Locale locale) {
- if (!Constants.isLetterCode(code) || !needsToUpperCase) return code;
- final String text = newSingleCodePointString(code);
- final String casedText = toUpperCaseOfStringForLocale(
- text, needsToUpperCase, locale);
- return codePointCount(casedText) == 1
- ? casedText.codePointAt(0) : CODE_UNSPECIFIED;
- }
-
- public static int getTrailingSingleQuotesCount(final CharSequence charSequence) {
- final int lastIndex = charSequence.length() - 1;
- int i = lastIndex;
- while (i >= 0 && charSequence.charAt(i) == Constants.CODE_SINGLE_QUOTE) {
- --i;
- }
- return lastIndex - i;
- }
-
- /**
- * Splits the given {@code charSequence} with at occurrences of the given {@code regex}.
- * <p>
- * This is equivalent to
- * {@code charSequence.toString().split(regex, preserveTrailingEmptySegments ? -1 : 0)}
- * except that the spans are preserved in the result array.
- * </p>
- * @param input the character sequence to be split.
- * @param regex the regex pattern to be used as the separator.
- * @param preserveTrailingEmptySegments {@code true} to preserve the trailing empty
- * segments. Otherwise, trailing empty segments will be removed before being returned.
- * @return the array which contains the result. All the spans in the {@param input} is
- * preserved.
- */
- @UsedForTesting
- public static CharSequence[] split(final CharSequence charSequence, final String regex,
- final boolean preserveTrailingEmptySegments) {
- // A short-cut for non-spanned strings.
- if (!(charSequence instanceof Spanned)) {
- // -1 means that trailing empty segments will be preserved.
- return charSequence.toString().split(regex, preserveTrailingEmptySegments ? -1 : 0);
- }
-
- // Hereafter, emulate String.split for CharSequence.
- final ArrayList<CharSequence> sequences = new ArrayList<>();
- final Matcher matcher = Pattern.compile(regex).matcher(charSequence);
- int nextStart = 0;
- boolean matched = false;
- while (matcher.find()) {
- sequences.add(charSequence.subSequence(nextStart, matcher.start()));
- nextStart = matcher.end();
- matched = true;
- }
- if (!matched) {
- // never matched. preserveTrailingEmptySegments is ignored in this case.
- return new CharSequence[] { charSequence };
- }
- sequences.add(charSequence.subSequence(nextStart, charSequence.length()));
- if (!preserveTrailingEmptySegments) {
- for (int i = sequences.size() - 1; i >= 0; --i) {
- if (!TextUtils.isEmpty(sequences.get(i))) {
- break;
- }
- sequences.remove(i);
- }
- }
- return sequences.toArray(new CharSequence[sequences.size()]);
- }
-
- @UsedForTesting
- public static class Stringizer<E> {
- public String stringize(final E element) {
- return element != null ? element.toString() : "null";
- }
-
- @UsedForTesting
- public final String join(final E[] array) {
- return joinStringArray(toStringArray(array), null /* delimiter */);
- }
-
- @UsedForTesting
- public final String join(final E[] array, final String delimiter) {
- return joinStringArray(toStringArray(array), delimiter);
- }
-
- protected String[] toStringArray(final E[] array) {
- final String[] stringArray = new String[array.length];
- for (int index = 0; index < array.length; index++) {
- stringArray[index] = stringize(array[index]);
- }
- return stringArray;
- }
-
- protected String joinStringArray(final String[] stringArray, final String delimiter) {
- if (stringArray == null) {
- return "null";
- }
- if (delimiter == null) {
- return Arrays.toString(stringArray);
- }
- final StringBuilder sb = new StringBuilder();
- for (int index = 0; index < stringArray.length; index++) {
- sb.append(index == 0 ? "[" : delimiter);
- sb.append(stringArray[index]);
- }
- return sb + "]";
- }
- }
-
- /**
- * Returns whether the last composed word contains line-breaking character (e.g. CR or LF).
- * @param text the text to be examined.
- * @return {@code true} if the last composed word contains line-breaking separator.
- */
- @UsedForTesting
- public static boolean hasLineBreakCharacter(final String text) {
- if (TextUtils.isEmpty(text)) {
- return false;
- }
- for (int i = text.length() - 1; i >= 0; --i) {
- final char c = text.charAt(i);
- switch (c) {
- case CHAR_LINE_FEED:
- case CHAR_VERTICAL_TAB:
- case CHAR_FORM_FEED:
- case CHAR_CARRIAGE_RETURN:
- case CHAR_NEXT_LINE:
- case CHAR_LINE_SEPARATOR:
- case CHAR_PARAGRAPH_SEPARATOR:
- return true;
- }
- }
- return false;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
index 351d01400..55c1dc9e5 100644
--- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
@@ -16,8 +16,9 @@
package com.android.inputmethod.latin.utils;
-import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
-import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
+import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.COMBINING_RULES;
+import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
import android.content.Context;
import android.content.res.Resources;
@@ -25,18 +26,23 @@ import android.os.Build;
import android.util.Log;
import android.view.inputmethod.InputMethodSubtype;
-import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
+import com.android.inputmethod.latin.common.StringUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
+/**
+ * A helper class to deal with subtype locales.
+ */
+// TODO: consolidate this into RichInputMethodSubtype
public final class SubtypeLocaleUtils {
- private static final String TAG = SubtypeLocaleUtils.class.getSimpleName();
+ static final String TAG = SubtypeLocaleUtils.class.getSimpleName();
- // This reference class {@link Constants} must be located in the same package as LatinIME.java.
- private static final String RESOURCE_PACKAGE_NAME = Constants.class.getPackage().getName();
+ // This reference class {@link R} must be located in the same package as LatinIME.java.
+ private static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName();
// Special language code to represent "no language".
public static final String NO_LANGUAGE = "zz";
@@ -52,6 +58,9 @@ public final class SubtypeLocaleUtils {
private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap = new HashMap<>();
// Keyboard layout to subtype name resource id map.
private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap = new HashMap<>();
+ // Exceptional locale whose name should be displayed in Locale.ROOT.
+ private static final HashMap<String, Integer> sExceptionalLocaleDisplayedInRootLocale =
+ new HashMap<>();
// Exceptional locale to subtype name resource id map.
private static final HashMap<String, Integer> sExceptionalLocaleToNameIdsMap = new HashMap<>();
// Exceptional locale to subtype name with layout resource id map.
@@ -65,6 +74,8 @@ public final class SubtypeLocaleUtils {
"string/subtype_with_layout_";
private static final String SUBTYPE_NAME_RESOURCE_NO_LANGUAGE_PREFIX =
"string/subtype_no_language_";
+ private static final String SUBTYPE_NAME_RESOURCE_IN_ROOT_LOCALE_PREFIX =
+ "string/subtype_in_root_locale_";
// 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 =
@@ -106,6 +117,15 @@ public final class SubtypeLocaleUtils {
sKeyboardLayoutToNameIdsMap.put(key, noLanguageResId);
}
+ final String[] exceptionalLocaleInRootLocale = res.getStringArray(
+ R.array.subtype_locale_displayed_in_root_locale);
+ for (int i = 0; i < exceptionalLocaleInRootLocale.length; i++) {
+ final String localeString = exceptionalLocaleInRootLocale[i];
+ final String resourceName = SUBTYPE_NAME_RESOURCE_IN_ROOT_LOCALE_PREFIX + localeString;
+ final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME);
+ sExceptionalLocaleDisplayedInRootLocale.put(localeString, resId);
+ }
+
final String[] exceptionalLocales = res.getStringArray(
R.array.subtype_locale_exception_keys);
for (int i = 0; i < exceptionalLocales.length; i++) {
@@ -153,10 +173,13 @@ public final class SubtypeLocaleUtils {
return nameId == null ? UNKNOWN_KEYBOARD_LAYOUT : nameId;
}
- private static Locale getDisplayLocaleOfSubtypeLocale(final String localeString) {
+ public static Locale getDisplayLocaleOfSubtypeLocale(final String localeString) {
if (NO_LANGUAGE.equals(localeString)) {
return sResources.getConfiguration().locale;
}
+ if (sExceptionalLocaleDisplayedInRootLocale.containsKey(localeString)) {
+ return Locale.ROOT;
+ }
return LocaleUtils.constructLocaleFromString(localeString);
}
@@ -171,9 +194,15 @@ public final class SubtypeLocaleUtils {
}
public static String getSubtypeLanguageDisplayName(final String localeString) {
- final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString);
- return getSubtypeLocaleDisplayNameInternal(locale.getLanguage(), displayLocale);
+ final String languageString;
+ if (sExceptionalLocaleDisplayedInRootLocale.containsKey(localeString)) {
+ languageString = localeString;
+ } else {
+ final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
+ languageString = locale.getLanguage();
+ }
+ return getSubtypeLocaleDisplayNameInternal(languageString, displayLocale);
}
private static String getSubtypeLocaleDisplayNameInternal(final String localeString,
@@ -182,7 +211,16 @@ public final class SubtypeLocaleUtils {
// No language subtype should be displayed in system locale.
return sResources.getString(R.string.subtype_no_language);
}
- final Integer exceptionalNameResId = sExceptionalLocaleToNameIdsMap.get(localeString);
+ final Integer exceptionalNameResId;
+ if (displayLocale.equals(Locale.ROOT)
+ && sExceptionalLocaleDisplayedInRootLocale.containsKey(localeString)) {
+ exceptionalNameResId = sExceptionalLocaleDisplayedInRootLocale.get(localeString);
+ } else if (sExceptionalLocaleToNameIdsMap.containsKey(localeString)) {
+ exceptionalNameResId = sExceptionalLocaleToNameIdsMap.get(localeString);
+ } else {
+ exceptionalNameResId = null;
+ }
+
final String displayName;
if (exceptionalNameResId != null) {
final RunInLocale<String> getExceptionalName = new RunInLocale<String>() {
@@ -222,9 +260,8 @@ public final class SubtypeLocaleUtils {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
&& subtype.containsExtraValueKey(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)) {
return subtype.getExtraValueOf(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME);
- } else {
- return getSubtypeLocaleDisplayNameInternal(subtype.getLocale(), displayLocale);
}
+ return getSubtypeLocaleDisplayNameInternal(subtype.getLocale(), displayLocale);
}
public static String getSubtypeDisplayNameInSystemLocale(final InputMethodSubtype subtype) {
@@ -242,6 +279,7 @@ public final class SubtypeLocaleUtils {
private static String getSubtypeDisplayNameInternal(final InputMethodSubtype subtype,
final Locale displayLocale) {
final String replacementString = getReplacementString(subtype, displayLocale);
+ // TODO: rework this for multi-lingual subtypes
final int nameResId = subtype.getNameResId();
final RunInLocale<String> getSubtypeName = new RunInLocale<String>() {
@Override
@@ -264,11 +302,6 @@ public final class SubtypeLocaleUtils {
getSubtypeName.runInLocale(sResources, displayLocale), displayLocale);
}
- public static boolean isNoLanguage(final InputMethodSubtype subtype) {
- final String localeString = subtype.getLocale();
- return NO_LANGUAGE.equals(localeString);
- }
-
public static Locale getSubtypeLocale(final InputMethodSubtype subtype) {
final String localeString = subtype.getLocale();
return LocaleUtils.constructLocaleFromString(localeString);
@@ -283,6 +316,10 @@ public final class SubtypeLocaleUtils {
return sKeyboardLayoutToDisplayNameMap.get(layoutName);
}
+ public static String getKeyboardLayoutSetName(final RichInputMethodSubtype subtype) {
+ return getKeyboardLayoutSetName(subtype.getRawSubtype());
+ }
+
public static String getKeyboardLayoutSetName(final InputMethodSubtype subtype) {
String keyboardLayoutSet = subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET);
if (keyboardLayoutSet == null) {
@@ -318,11 +355,7 @@ public final class SubtypeLocaleUtils {
return Arrays.binarySearch(SORTED_RTL_LANGUAGES, language) >= 0;
}
- public static boolean isRtlLanguage(final InputMethodSubtype subtype) {
- return isRtlLanguage(getSubtypeLocale(subtype));
- }
-
public static String getCombiningRulesExtraValue(final InputMethodSubtype subtype) {
- return subtype.getExtraValueOf(Constants.Subtype.ExtraValue.COMBINING_RULES);
+ return subtype.getExtraValueOf(COMBINING_RULES);
}
}
diff --git a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java
index 8cd49509f..b319aeb8a 100644
--- a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java
+++ b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java
@@ -22,7 +22,6 @@ import com.android.inputmethod.latin.define.ProductionFlags;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
-import java.util.Locale;
import java.util.TreeSet;
/**
@@ -30,22 +29,19 @@ import java.util.TreeSet;
* than its limit
*/
public final class SuggestionResults extends TreeSet<SuggestedWordInfo> {
- public final Locale mLocale;
public final ArrayList<SuggestedWordInfo> mRawSuggestions;
// TODO: Instead of a boolean , we may want to include the context of this suggestion results,
- // such as {@link PrevWordsInfo}.
+ // such as {@link NgramContext}.
public final boolean mIsBeginningOfSentence;
private final int mCapacity;
- public SuggestionResults(final Locale locale, final int capacity,
- final boolean isBeginningOfSentence) {
- this(locale, sSuggestedWordInfoComparator, capacity, isBeginningOfSentence);
+ public SuggestionResults(final int capacity, final boolean isBeginningOfSentence) {
+ this(sSuggestedWordInfoComparator, capacity, isBeginningOfSentence);
}
- private SuggestionResults(final Locale locale, final Comparator<SuggestedWordInfo> comparator,
+ private SuggestionResults(final Comparator<SuggestedWordInfo> comparator,
final int capacity, final boolean isBeginningOfSentence) {
super(comparator);
- mLocale = locale;
mCapacity = capacity;
if (ProductionFlags.INCLUDE_RAW_SUGGESTIONS) {
mRawSuggestions = new ArrayList<>();
@@ -70,8 +66,7 @@ public final class SuggestionResults extends TreeSet<SuggestedWordInfo> {
return super.addAll(e);
}
- private static final class SuggestedWordInfoComparator
- implements Comparator<SuggestedWordInfo> {
+ static final 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
diff --git a/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java b/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java
index dd122b634..0bcba2754 100644
--- a/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java
@@ -57,7 +57,7 @@ public final class ViewLayoutUtils {
public static void updateLayoutHeightOf(final Window window, final int layoutHeight) {
final WindowManager.LayoutParams params = window.getAttributes();
- if (params.height != layoutHeight) {
+ if (params != null && params.height != layoutHeight) {
params.height = layoutHeight;
window.setAttributes(params);
}
@@ -65,7 +65,7 @@ public final class ViewLayoutUtils {
public static void updateLayoutHeightOf(final View view, final int layoutHeight) {
final ViewGroup.LayoutParams params = view.getLayoutParams();
- if (params.height != layoutHeight) {
+ if (params != null && params.height != layoutHeight) {
params.height = layoutHeight;
view.setLayoutParams(params);
}
diff --git a/java/src/com/android/inputmethod/latin/utils/WordInputEventForPersonalization.java b/java/src/com/android/inputmethod/latin/utils/WordInputEventForPersonalization.java
new file mode 100644
index 000000000..86a5b19ec
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/WordInputEventForPersonalization.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2014 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.utils;
+
+import android.util.Log;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.NgramContext;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
+import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
+import com.android.inputmethod.latin.utils.DistracterFilter.HandlingType;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+// Note: this class is used as a parameter type of a native method. You should be careful when you
+// rename this class or field name. See BinaryDictionary#addMultipleDictionaryEntriesNative().
+public final class WordInputEventForPersonalization {
+ private static final String TAG = WordInputEventForPersonalization.class.getSimpleName();
+ private static final boolean DEBUG_TOKEN = false;
+
+ public final int[] mTargetWord;
+ public final int mPrevWordsCount;
+ public final int[][] mPrevWordArray = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][];
+ public final boolean[] mIsPrevWordBeginningOfSentenceArray =
+ new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
+ public final boolean mIsValid;
+ // Time stamp in seconds.
+ public final int mTimestamp;
+
+ @UsedForTesting
+ public WordInputEventForPersonalization(final CharSequence targetWord,
+ final NgramContext ngramContext, final boolean isValid, final int timestamp) {
+ mTargetWord = StringUtils.toCodePointArray(targetWord);
+ mPrevWordsCount = ngramContext.getPrevWordCount();
+ ngramContext.outputToArray(mPrevWordArray, mIsPrevWordBeginningOfSentenceArray);
+ mIsValid = isValid;
+ mTimestamp = timestamp;
+ }
+
+ // Process a list of words and return a list of {@link WordInputEventForPersonalization}
+ // objects.
+ public static ArrayList<WordInputEventForPersonalization> createInputEventFrom(
+ final List<String> tokens, final int timestamp,
+ final SpacingAndPunctuations spacingAndPunctuations, final Locale locale,
+ final DistracterFilter distracterFilter) {
+ final ArrayList<WordInputEventForPersonalization> inputEvents = new ArrayList<>();
+ final int N = tokens.size();
+ NgramContext ngramContext = NgramContext.EMPTY_PREV_WORDS_INFO;
+ for (int i = 0; i < N; ++i) {
+ final String tempWord = tokens.get(i);
+ if (StringUtils.isEmptyStringOrWhiteSpaces(tempWord)) {
+ // just skip this token
+ if (DEBUG_TOKEN) {
+ Log.d(TAG, "--- isEmptyStringOrWhiteSpaces: \"" + tempWord + "\"");
+ }
+ continue;
+ }
+ if (!DictionaryInfoUtils.looksValidForDictionaryInsertion(
+ tempWord, spacingAndPunctuations)) {
+ if (DEBUG_TOKEN) {
+ Log.d(TAG, "--- not looksValidForDictionaryInsertion: \""
+ + tempWord + "\"");
+ }
+ // Sentence terminator found. Split.
+ // TODO: Detect whether the context is beginning-of-sentence.
+ ngramContext = NgramContext.EMPTY_PREV_WORDS_INFO;
+ continue;
+ }
+ if (DEBUG_TOKEN) {
+ Log.d(TAG, "--- word: \"" + tempWord + "\"");
+ }
+ final WordInputEventForPersonalization inputEvent =
+ detectWhetherVaildWordOrNotAndGetInputEvent(
+ ngramContext, tempWord, timestamp, locale, distracterFilter);
+ if (inputEvent == null) {
+ continue;
+ }
+ inputEvents.add(inputEvent);
+ ngramContext = ngramContext.getNextNgramContext(new NgramContext.WordInfo(tempWord));
+ }
+ return inputEvents;
+ }
+
+ private static WordInputEventForPersonalization detectWhetherVaildWordOrNotAndGetInputEvent(
+ final NgramContext ngramContext, final String targetWord, final int timestamp,
+ final Locale locale, final DistracterFilter distracterFilter) {
+ if (locale == null) {
+ return null;
+ }
+ final int wordHandlingType = distracterFilter.getWordHandlingType(ngramContext,
+ targetWord, locale);
+ final String word = HandlingType.shouldBeLowerCased(wordHandlingType) ?
+ targetWord.toLowerCase(locale) : targetWord;
+ if (distracterFilter.isDistracterToWordsInDictionaries(ngramContext, targetWord, locale)) {
+ // The word is a distracter.
+ return null;
+ }
+ return new WordInputEventForPersonalization(word, ngramContext,
+ !HandlingType.shouldBeHandledAsOov(wordHandlingType), timestamp);
+ }
+}