diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
138 files changed, 7713 insertions, 8076 deletions
diff --git a/java/src/com/android/inputmethod/latin/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java index fd6c24dfe..f8d02d6ea 100644 --- a/java/src/com/android/inputmethod/latin/AssetFileAddress.java +++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java @@ -16,7 +16,7 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.latin.utils.FileUtils; +import com.android.inputmethod.latin.common.FileUtils; import java.io.File; @@ -62,4 +62,9 @@ public final class AssetFileAddress { public void deleteUnderlyingFile() { FileUtils.deleteRecursively(new File(mFilename)); } + + @Override + public String toString() { + return String.format("%s (offset=%d, length=%d)", mFilename, mOffset, mLength); + } } 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..9a3ac674e 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -21,8 +21,12 @@ 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.FileUtils; +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; @@ -30,10 +34,8 @@ import com.android.inputmethod.latin.makedict.UnsupportedFormatException; import com.android.inputmethod.latin.makedict.WordProperty; 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. */ @@ -53,6 +57,9 @@ public final class BinaryDictionary extends Dictionary { // Must be equal to CONFIDENCE_TO_AUTO_COMMIT in native/jni/src/defines.h private static final int CONFIDENCE_TO_AUTO_COMMIT = 1000000; + public static final int DICTIONARY_MAX_WORD_LENGTH = 48; + public static final int MAX_PREV_WORD_COUNT_FOR_N_GRAM = 3; + @UsedForTesting public static final String UNIGRAM_COUNT_QUERY = "UNIGRAM_COUNT"; @UsedForTesting @@ -67,9 +74,9 @@ 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_HAS_SHORTCUTS_INDEX = 3; + 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; // DEPRECATED private static final int FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX = 4; // Format to get probability and historical info from native side via getWordPropertyNative(). @@ -83,7 +90,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 +123,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 +143,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,36 +184,41 @@ 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, long newFormatVersion); // TODO: Move native dict into session - private final void loadDictionary(final String path, final long startOffset, + private void loadDictionary(final String path, final long startOffset, final long length, final boolean isUpdatable) { mHasUpdated = false; mNativeDict = openNative(path, startOffset, length, isUpdatable); @@ -256,23 +265,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; } @@ -283,41 +294,45 @@ public final class BinaryDictionary extends Dictionary { session.mNativeSuggestOptions.setIsGesture(isGesture); session.mNativeSuggestOptions.setBlockOffensiveWords( settingsValuesForSuggestion.mBlockPotentiallyOffensive); - session.mNativeSuggestOptions.setSpaceAwareGestureEnabled( - 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<>(); for (int j = 0; j < count; ++j) { - final int start = j * Constants.DICTIONARY_MAX_WORD_LENGTH; + final int start = j * DICTIONARY_MAX_WORD_LENGTH; int len = 0; - while (len < Constants.DICTIONARY_MAX_WORD_LENGTH + while (len < DICTIONARY_MAX_WORD_LENGTH && session.mOutputCodePoints[start + len] != 0) { ++len; } if (len > 0) { suggestions.add(new SuggestedWordInfo( new String(session.mOutputCodePoints, start, len), - session.mOutputScores[j], session.mOutputTypes[j], this /* sourceDict */, + "" /* prevWordsContext */, + (int)(session.mOutputScores[j] * weightForLocale), + session.mOutputTypes[j], + this /* sourceDict */, session.mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */, session.mOutputAutoCommitFirstWordConfidence[0])); } @@ -340,31 +355,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); @@ -375,25 +393,28 @@ public final class BinaryDictionary extends Dictionary { return null; } final int[] codePoints = StringUtils.toCodePointArray(word); - final int[] outCodePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH]; + final int[] outCodePoints = new int[DICTIONARY_MAX_WORD_LENGTH]; 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_HAS_SHORTCUTS_INDEX], + outFlags[FORMAT_WORD_PROPERTY_IS_POSSIBLY_OFFENSIVE_INDEX], + outFlags[FORMAT_WORD_PROPERTY_HAS_NGRAMS_INDEX], outFlags[FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX], outProbabilityInfo, - outBigramTargets, outBigramProbabilityInfo, outShortcutTargets, - outShortcutProbabilities); + outNgramPrevWordsArray, outNgramPrevWordIsBeginningOfSentenceArray, + outNgramTargets, outNgramProbabilityInfo); } public static class GetNextWordPropertyResult { @@ -411,7 +432,7 @@ public final class BinaryDictionary extends Dictionary { * If token is 0, this method newly starts iterating the dictionary. */ public GetNextWordPropertyResult getNextWordProperty(final int token) { - final int[] codePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH]; + final int[] codePoints = new int[DICTIONARY_MAX_WORD_LENGTH]; final boolean[] isBeginningOfSentence = new boolean[1]; final int nextToken = getNextWordNative(mNativeDict, token, codePoints, isBeginningOfSentence); @@ -421,18 +442,16 @@ public final class BinaryDictionary extends Dictionary { } // Add a unigram entry to binary dictionary with unigram attributes in native code. - 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) { + public boolean addUnigramEntry( + final String word, final int probability, final boolean isBeginningOfSentence, + final boolean isNotAWord, final boolean isPossiblyOffensive, final int timestamp) { if (word == null || (word.isEmpty() && !isBeginningOfSentence)) { return false; } final int[] codePoints = StringUtils.toCodePointArray(word); - final int[] shortcutTargetCodePoints = (shortcutTarget != null) ? - StringUtils.toCodePointArray(shortcutTarget) : null; - if (!addUnigramEntryNative(mNativeDict, codePoints, probability, shortcutTargetCodePoints, - shortcutProbability, isBeginningOfSentence, isNotAWord, isBlacklisted, timestamp)) { + if (!addUnigramEntryNative(mNativeDict, codePoints, probability, + null /* shortcutTargetCodePoints */, 0 /* shortcutProbability */, + isBeginningOfSentence, isNotAWord, isPossiblyOffensive, timestamp)) { return false; } mHasUpdated = true; @@ -453,15 +472,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)) { @@ -471,35 +489,38 @@ public final class BinaryDictionary extends Dictionary { return true; } - // 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)) { + // 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[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)) { + if (!updateEntriesForWordWithNgramContextNative(mNativeDict, prevWordCodePointArrays, + isBeginningOfSentenceArray, wordCodePoints, isValidWord, count, timestamp)) { return false; } mHasUpdated = true; return true; } - public void addMultipleDictionaryEntries(final LanguageModelParam[] languageModelParams) { - if (!isValidDictionary()) return; - int processedParamCount = 0; - while (processedParamCount < languageModelParams.length) { + @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 +538,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 +560,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 +577,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 +623,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/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index 10b1f1b77..bc62f3ae3 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -29,6 +29,7 @@ import android.util.Log; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; import com.android.inputmethod.dictionarypack.MD5Calculator; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils.DictionaryInfo; import com.android.inputmethod.latin.utils.FileTransforms; @@ -67,6 +68,11 @@ public final class BinaryDictionaryFileDumper { private static final byte[] MAGIC_NUMBER_VERSION_2 = new byte[] { (byte)0x9B, (byte)0xC1, (byte)0x3A, (byte)0xFE }; + private static final boolean SHOULD_VERIFY_MAGIC_NUMBER = + DecoderSpecificConstants.SHOULD_VERIFY_MAGIC_NUMBER; + private static final boolean SHOULD_VERIFY_CHECKSUM = + DecoderSpecificConstants.SHOULD_VERIFY_CHECKSUM; + private static final String DICTIONARY_PROJECTION[] = { "id" }; private static final String QUERY_PARAMETER_MAY_PROMPT_USER = "mayPrompt"; @@ -302,13 +308,18 @@ public final class BinaryDictionaryFileDumper { checkMagicAndCopyFileTo(bufferedInputStream, bufferedOutputStream); bufferedOutputStream.flush(); bufferedOutputStream.close(); - final String actualRawChecksum = MD5Calculator.checksum( - new BufferedInputStream(new FileInputStream(outputFile))); - Log.i(TAG, "Computed checksum for downloaded dictionary. Expected = " + rawChecksum - + " ; actual = " + actualRawChecksum); - if (!TextUtils.isEmpty(rawChecksum) && !rawChecksum.equals(actualRawChecksum)) { - throw new IOException("Could not decode the file correctly : checksum differs"); + + if (SHOULD_VERIFY_CHECKSUM) { + final String actualRawChecksum = MD5Calculator.checksum( + new BufferedInputStream(new FileInputStream(outputFile))); + Log.i(TAG, "Computed checksum for downloaded dictionary. Expected = " + + rawChecksum + " ; actual = " + actualRawChecksum); + if (!TextUtils.isEmpty(rawChecksum) && !rawChecksum.equals(actualRawChecksum)) { + throw new IOException( + "Could not decode the file correctly : checksum differs"); + } } + final File finalFile = new File(finalFileName); finalFile.delete(); if (!outputFile.renameTo(finalFile)) { @@ -444,9 +455,11 @@ public final class BinaryDictionaryFileDumper { if (readMagicNumberSize < length) { throw new IOException("Less bytes to read than the magic number length"); } - if (!Arrays.equals(MAGIC_NUMBER_VERSION_2, magicNumberBuffer)) { - if (!Arrays.equals(MAGIC_NUMBER_VERSION_1, magicNumberBuffer)) { - throw new IOException("Wrong magic number for downloaded file"); + if (SHOULD_VERIFY_MAGIC_NUMBER) { + if (!Arrays.equals(MAGIC_NUMBER_VERSION_2, magicNumberBuffer)) { + if (!Arrays.equals(MAGIC_NUMBER_VERSION_1, magicNumberBuffer)) { + throw new IOException("Wrong magic number for downloaded file"); + } } } output.write(magicNumberBuffer); diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index 867c18686..f4300c462 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -21,11 +21,12 @@ import android.content.SharedPreferences; import android.content.res.AssetFileDescriptor; import android.util.Log; +import com.android.inputmethod.latin.common.LocaleUtils; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.makedict.UnsupportedFormatException; import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; -import com.android.inputmethod.latin.utils.LocaleUtils; import java.io.File; import java.io.IOException; @@ -54,6 +55,9 @@ final public class BinaryDictionaryGetter { */ private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs"; + private static final boolean SHOULD_USE_DICT_VERSION = + DecoderSpecificConstants.SHOULD_USE_DICT_VERSION; + // Name of the category for the main dictionary public static final String MAIN_DICTIONARY_CATEGORY = "main"; public static final String ID_CATEGORY_SEPARATOR = ":"; @@ -87,10 +91,15 @@ final public class BinaryDictionaryGetter { */ public static AssetFileAddress loadFallbackResource(final Context context, final int fallbackResId) { - final AssetFileDescriptor afd = context.getResources().openRawResourceFd(fallbackResId); + AssetFileDescriptor afd = null; + try { + afd = context.getResources().openRawResourceFd(fallbackResId); + } catch (RuntimeException e) { + Log.e(TAG, "Resource not found: " + fallbackResId, e); + return null; + } if (afd == null) { - Log.e(TAG, "Found the resource but cannot read it. Is it compressed? resId=" - + fallbackResId); + Log.e(TAG, "Resource cannot be opened: " + fallbackResId); return null; } try { @@ -99,8 +108,7 @@ final public class BinaryDictionaryGetter { } finally { try { afd.close(); - } catch (IOException e) { - // Ignored + } catch (IOException ignored) { } } } @@ -121,12 +129,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 +231,11 @@ 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) { + if (!SHOULD_USE_DICT_VERSION) { + return true; + } + try { // Read the version of the file final DictionaryHeader header = BinaryDictionaryUtils.getHeader(file); @@ -264,7 +275,8 @@ final public class BinaryDictionaryGetter { public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale, final Context context) { - final boolean hasDefaultWordList = DictionaryFactory.isDictionaryAvailable(context, locale); + final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable( + context, locale); BinaryDictionaryFileDumper.cacheWordListsFromContentProvider(locale, context, hasDefaultWordList); final File[] cachedWordLists = getCachedWordLists(locale.toString(), context); @@ -276,7 +288,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; } @@ -285,7 +297,8 @@ final public class BinaryDictionaryGetter { final AssetFileAddress afa = AssetFileAddress.makeFromFileName(f.getPath()); if (null != afa) fileList.add(afa); } else { - Log.e(TAG, "Found a cached dictionary file but cannot read or use it"); + Log.e(TAG, "Found a cached dictionary file for " + locale.toString() + + " but cannot read or use it"); } } 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..15a14e5af 100644 --- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java @@ -16,34 +16,26 @@ package com.android.inputmethod.latin; -import android.content.ContentResolver; import android.content.Context; -import android.database.ContentObserver; -import android.database.Cursor; -import android.database.sqlite.SQLiteException; import android.net.Uri; -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.ContactsManager.ContactsChangedListener; +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; import java.util.List; import java.util.Locale; -public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { - - private static final String[] PROJECTION = {BaseColumns._ID, Contacts.DISPLAY_NAME}; - private static final String[] PROJECTION_ID_ONLY = {BaseColumns._ID}; +import javax.annotation.Nullable; +public class ContactsBinaryDictionary extends ExpandableBinaryDictionary + implements ContactsChangedListener { private static final String TAG = ContactsBinaryDictionary.class.getSimpleName(); private static final String NAME = "contacts"; @@ -51,72 +43,39 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { private static final boolean DEBUG_DUMP = false; /** - * Frequency for contacts information into the dictionary - */ - private static final int FREQUENCY_FOR_CONTACTS = 40; - private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90; - - /** The maximum number of contacts that this dictionary supports. */ - private static final int MAX_CONTACT_COUNT = 10000; - - private static final int INDEX_NAME = 1; - - /** The number of contacts in the most recent dictionary rebuild. */ - private int mContactCountAtLastRebuild = 0; - - /** The hash code of ArrayList of contacts names in the most recent dictionary rebuild. */ - private int mHashCodeAtLastRebuild = 0; - - private ContentObserver mObserver; - - /** * Whether to use "firstname lastname" in bigram predictions. */ private final boolean mUseFirstLastBigrams; + private final ContactsManager mContactsManager; protected ContactsBinaryDictionary(final Context context, final Locale locale, final File dictFile, final String name) { super(context, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_CONTACTS, dictFile); - mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale); - registerObserver(context); + mUseFirstLastBigrams = ContactsDictionaryUtils.useFirstLastBigramsForLocale(locale); + mContactsManager = new ContactsManager(context); + mContactsManager.registerForUpdates(this /* listener */); 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) { + final File dictFile, final String dictNamePrefix, @Nullable final String account) { return new ContactsBinaryDictionary(context, locale, dictFile, dictNamePrefix + NAME); } - private synchronized void registerObserver(final Context context) { - if (mObserver != null) return; - ContentResolver cres = context.getContentResolver(); - cres.registerContentObserver(Contacts.CONTENT_URI, true, mObserver = - new ContentObserver(null) { - @Override - public void onChange(boolean self) { - ExecutorUtils.getExecutor("Check Contacts").execute(new Runnable() { - @Override - public void run() { - if (haveContentsChanged()) { - setNeedsToRecreate(); - } - } - }); - } - }); - } - @Override public synchronized void close() { - if (mObserver != null) { - mContext.getContentResolver().unregisterContentObserver(mObserver); - mObserver = null; - } + mContactsManager.close(); super.close(); } + /** + * Typically called whenever the dictionary is created for the first time or + * recreated when we think that there are updates to the dictionary. + * This is called asynchronously. + */ @Override public void loadInitialContentsLocked() { loadDeviceAccountsEmailAddressesLocked(); @@ -125,6 +84,9 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { loadDictionaryForUriLocked(Contacts.CONTENT_URI); } + /** + * Loads device accounts to the dictionary. + */ private void loadDeviceAccountsEmailAddressesLocked() { final List<String> accountVocabulary = AccountUtils.getDeviceAccountsEmailAddresses(mContext); @@ -136,80 +98,25 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { Log.d(TAG, "loadAccountVocabulary: " + word); } runGCIfRequiredLocked(true /* mindsBlockByGC */); - addUnigramLocked(word, FREQUENCY_FOR_CONTACTS, null /* shortcut */, - 0 /* shortcutFreq */, false /* isNotAWord */, false /* isBlacklisted */, + addUnigramLocked(word, ContactsDictionaryConstants.FREQUENCY_FOR_CONTACTS, + false /* isNotAWord */, false /* isPossiblyOffensive */, BinaryDictionary.NOT_A_VALID_TIMESTAMP); } } + /** + * Loads data within content providers to the dictionary. + */ private void loadDictionaryForUriLocked(final Uri uri) { - Cursor cursor = null; - try { - cursor = mContext.getContentResolver().query(uri, PROJECTION, null, null, null); - if (null == cursor) { - return; - } - if (cursor.moveToFirst()) { - mContactCountAtLastRebuild = getContactCount(); - addWordsLocked(cursor); - } - } catch (final SQLiteException e) { - Log.e(TAG, "SQLiteException in the remote Contacts process.", e); - } catch (final IllegalStateException e) { - Log.e(TAG, "Contacts DB is having problems", e); - } finally { - if (null != cursor) { - cursor.close(); - } - } - } - - private 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; - } - return false; - } - - private void addWordsLocked(final Cursor cursor) { - int count = 0; - final ArrayList<String> names = new ArrayList<>(); - while (!cursor.isAfterLast() && count < MAX_CONTACT_COUNT) { - String name = cursor.getString(INDEX_NAME); - if (isValidName(name)) { - names.add(name); - addNameLocked(name); - ++count; - } else { - if (DEBUG_DUMP) { - Log.d(TAG, "Invalid name: " + name); - } - } - cursor.moveToNext(); + final ArrayList<String> validNames = mContactsManager.getValidNames(uri); + for (final String name : validNames) { + addNameLocked(name); } - mHashCodeAtLastRebuild = names.hashCode(); - } - - private int getContactCount() { - // TODO: consider switching to a rawQuery("select count(*)...") on the database if - // performance is a bottleneck. - Cursor cursor = null; - try { - cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI, PROJECTION_ID_ONLY, - null, null, null); - if (null == cursor) { - return 0; - } - return cursor.getCount(); - } catch (final SQLiteException e) { - Log.e(TAG, "SQLiteException in the remote Contacts process.", e); - } finally { - if (null != cursor) { - cursor.close(); - } + if (uri.equals(Contacts.CONTENT_URI)) { + // Since we were able to add content successfully, update the local + // state of the manager. + mContactsManager.updateLocalState(validNames); } - return 0; } /** @@ -218,11 +125,12 @@ 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.getEmptyPrevWordsContext( + BinaryDictionary.MAX_PREV_WORD_COUNT_FOR_N_GRAM); // TODO: Better tokenization for non-Latin writing systems for (int i = 0; i < len; i++) { if (Character.isLetter(name.codePointAt(i))) { - int end = getWordEndPosition(name, len, i); + int end = ContactsDictionaryUtils.getWordEndPosition(name, len, i); String word = name.substring(i, end); if (DEBUG_DUMP) { Log.d(TAG, "addName word = " + word); @@ -233,93 +141,29 @@ 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) { + addUnigramLocked(word, + ContactsDictionaryConstants.FREQUENCY_FOR_CONTACTS, false /* isNotAWord */, + false /* isPossiblyOffensive */, + BinaryDictionary.NOT_A_VALID_TIMESTAMP); + if (ngramContext.isValid() && mUseFirstLastBigrams) { runGCIfRequiredLocked(true /* mindsBlockByGC */); - addNgramEntryLocked(prevWordsInfo, word, FREQUENCY_FOR_CONTACTS_BIGRAM, + addNgramEntryLocked(ngramContext, + word, + ContactsDictionaryConstants.FREQUENCY_FOR_CONTACTS_BIGRAM, BinaryDictionary.NOT_A_VALID_TIMESTAMP); } - prevWordsInfo = prevWordsInfo.getNextPrevWordsInfo( - new PrevWordsInfo.WordInfo(word)); - } - } - } - } - - /** - * Returns the index of the last letter in the word, starting from position startIndex. - */ - private static int getWordEndPosition(final String string, final int len, - final int startIndex) { - int end; - int cp = 0; - for (end = startIndex + 1; end < len; end += Character.charCount(cp)) { - cp = string.codePointAt(end); - if (!(cp == Constants.CODE_DASH || cp == Constants.CODE_SINGLE_QUOTE - || Character.isLetter(cp))) { - break; - } - } - return end; - } - - private boolean haveContentsChanged() { - final long startTime = SystemClock.uptimeMillis(); - final int contactCount = getContactCount(); - if (contactCount > MAX_CONTACT_COUNT) { - // If there are too many contacts then return false. In this rare case it is impossible - // to include all of them anyways and the cost of rebuilding the dictionary is too high. - // TODO: Sort and check only the MAX_CONTACT_COUNT most recent contacts? - return false; - } - if (contactCount != mContactCountAtLastRebuild) { - if (DEBUG) { - Log.d(TAG, "Contact count changed: " + mContactCountAtLastRebuild + " to " - + contactCount); - } - return true; - } - // Check all contacts since it's not possible to find out which names have changed. - // This is needed because it's possible to receive extraneous onChange events even when no - // name has changed. - final Cursor cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI, PROJECTION, - null, null, null); - if (null == cursor) { - return false; - } - final ArrayList<String> names = new ArrayList<>(); - try { - if (cursor.moveToFirst()) { - while (!cursor.isAfterLast()) { - String name = cursor.getString(INDEX_NAME); - if (isValidName(name)) { - names.add(name); - } - cursor.moveToNext(); + ngramContext = ngramContext.getNextNgramContext( + new NgramContext.WordInfo(word)); } } - if (names.hashCode() != mHashCodeAtLastRebuild) { - return true; - } - } finally { - cursor.close(); } - if (DEBUG) { - Log.d(TAG, "No contacts changed. (runtime = " + (SystemClock.uptimeMillis() - startTime) - + " ms)"); - } - return false; } - private static boolean isValidName(final String name) { - if (name != null && -1 == name.indexOf(Constants.CODE_COMMERCIAL_AT)) { - return true; - } - return false; + @Override + public void onContactsChange() { + setNeedsToRecreate(); } } diff --git a/java/src/com/android/inputmethod/latin/ContactsContentObserver.java b/java/src/com/android/inputmethod/latin/ContactsContentObserver.java new file mode 100644 index 000000000..5eb9b16d1 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/ContactsContentObserver.java @@ -0,0 +1,114 @@ +/* + * 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.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.os.SystemClock; +import android.provider.ContactsContract.Contacts; +import android.util.Log; + +import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener; +import com.android.inputmethod.latin.utils.ExecutorUtils; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A content observer that listens to updates to content provider {@link Contacts#CONTENT_URI}. + */ +public class ContactsContentObserver implements Runnable { + private static final String TAG = ContactsContentObserver.class.getSimpleName(); + private static final boolean DEBUG = false; + private static AtomicBoolean sRunning = new AtomicBoolean(false); + + private final Context mContext; + private final ContactsManager mManager; + + private ContentObserver mContentObserver; + private ContactsChangedListener mContactsChangedListener; + + public ContactsContentObserver(final ContactsManager manager, final Context context) { + mManager = manager; + mContext = context; + } + + public void registerObserver(final ContactsChangedListener listener) { + if (DEBUG) { + Log.d(TAG, "Registered Contacts Content Observer"); + } + mContactsChangedListener = listener; + mContentObserver = new ContentObserver(null /* handler */) { + @Override + public void onChange(boolean self) { + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD) + .execute(ContactsContentObserver.this); + } + }; + final ContentResolver contentResolver = mContext.getContentResolver(); + contentResolver.registerContentObserver(Contacts.CONTENT_URI, true, mContentObserver); + } + + @Override + public void run() { + if (!sRunning.compareAndSet(false /* expect */, true /* update */)) { + if (DEBUG) { + Log.d(TAG, "run() : Already running. Don't waste time checking again."); + } + return; + } + if (haveContentsChanged()) { + if (DEBUG) { + Log.d(TAG, "run() : Contacts have changed. Notifying listeners."); + } + mContactsChangedListener.onContactsChange(); + } + sRunning.set(false); + } + + boolean haveContentsChanged() { + final long startTime = SystemClock.uptimeMillis(); + final int contactCount = mManager.getContactCount(); + if (contactCount > ContactsDictionaryConstants.MAX_CONTACT_COUNT) { + // If there are too many contacts then return false. In this rare case it is impossible + // to include all of them anyways and the cost of rebuilding the dictionary is too high. + // TODO: Sort and check only the MAX_CONTACT_COUNT most recent contacts? + return false; + } + if (contactCount != mManager.getContactCountAtLastRebuild()) { + if (DEBUG) { + Log.d(TAG, "Contact count changed: " + mManager.getContactCountAtLastRebuild() + + " to " + contactCount); + } + return true; + } + final ArrayList<String> names = mManager.getValidNames(Contacts.CONTENT_URI); + if (names.hashCode() != mManager.getHashCodeAtLastRebuild()) { + return true; + } + if (DEBUG) { + Log.d(TAG, "No contacts changed. (runtime = " + (SystemClock.uptimeMillis() - startTime) + + " ms)"); + } + return false; + } + + public void unregister() { + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } +} diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionaryConstants.java b/java/src/com/android/inputmethod/latin/ContactsDictionaryConstants.java new file mode 100644 index 000000000..8d8faca58 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/ContactsDictionaryConstants.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 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.provider.BaseColumns; +import android.provider.ContactsContract.Contacts; + +/** + * Constants related to Contacts Content Provider. + */ +public class ContactsDictionaryConstants { + /** + * Projections for {@link Contacts.CONTENT_URI} + */ + public static final String[] PROJECTION = { BaseColumns._ID, Contacts.DISPLAY_NAME }; + public static final String[] PROJECTION_ID_ONLY = { BaseColumns._ID }; + + /** + * Frequency for contacts information into the dictionary + */ + public static final int FREQUENCY_FOR_CONTACTS = 40; + public static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90; + + /** + * The maximum number of contacts that this dictionary supports. + */ + public static final int MAX_CONTACT_COUNT = 10000; + + /** + * Index of the column for 'name' in content providers: + * Contacts & ContactsContract.Profile. + */ + public static final int NAME_INDEX = 1; +} diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionaryUtils.java b/java/src/com/android/inputmethod/latin/ContactsDictionaryUtils.java new file mode 100644 index 000000000..b77388434 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/ContactsDictionaryUtils.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; + +import com.android.inputmethod.latin.common.Constants; + +import java.util.Locale; + +/** + * Utility methods related contacts dictionary. + */ +public class ContactsDictionaryUtils { + + /** + * Returns the index of the last letter in the word, starting from position startIndex. + */ + public static int getWordEndPosition(final String string, final int len, + final int startIndex) { + int end; + int cp = 0; + for (end = startIndex + 1; end < len; end += Character.charCount(cp)) { + cp = string.codePointAt(end); + if (cp != Constants.CODE_DASH && cp != Constants.CODE_SINGLE_QUOTE + && !Character.isLetter(cp)) { + break; + } + } + return end; + } + + /** + * Returns true if the locale supports using first name and last name as bigrams. + */ + public 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; + } + return false; + } +} diff --git a/java/src/com/android/inputmethod/latin/ContactsManager.java b/java/src/com/android/inputmethod/latin/ContactsManager.java new file mode 100644 index 000000000..1fadc6f6f --- /dev/null +++ b/java/src/com/android/inputmethod/latin/ContactsManager.java @@ -0,0 +1,159 @@ +/* + * 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.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.net.Uri; +import android.provider.ContactsContract.Contacts; +import android.util.Log; + +import com.android.inputmethod.latin.common.Constants; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Manages all interactions with Contacts DB. + * + * The manager provides an API for listening to meaning full updates by keeping a + * measure of the current state of the content provider. + */ +public class ContactsManager { + private static final String TAG = ContactsManager.class.getSimpleName(); + private static final boolean DEBUG = false; + + /** + * Interface to implement for classes interested in getting notified for updates + * to Contacts content provider. + */ + public static interface ContactsChangedListener { + public void onContactsChange(); + } + + /** + * The number of contacts observed in the most recent instance of + * contacts content provider. + */ + private AtomicInteger mContactCountAtLastRebuild = new AtomicInteger(0); + + /** + * The hash code of list of valid contacts names in the most recent dictionary + * rebuild. + */ + private AtomicInteger mHashCodeAtLastRebuild = new AtomicInteger(0); + + private final Context mContext; + private final ContactsContentObserver mObserver; + + public ContactsManager(final Context context) { + mContext = context; + mObserver = new ContactsContentObserver(this /* ContactsManager */, context); + } + + // TODO: This was synchronized in previous version. Why? + public void registerForUpdates(final ContactsChangedListener listener) { + mObserver.registerObserver(listener); + } + + public int getContactCountAtLastRebuild() { + return mContactCountAtLastRebuild.get(); + } + + public int getHashCodeAtLastRebuild() { + return mHashCodeAtLastRebuild.get(); + } + + /** + * Returns all the valid names in the Contacts DB. Callers should also + * call {@link #updateLocalState(ArrayList)} after they are done with result + * so that the manager can cache local state for determining updates. + */ + public ArrayList<String> getValidNames(final Uri uri) { + final ArrayList<String> names = new ArrayList<>(); + // Check all contacts since it's not possible to find out which names have changed. + // This is needed because it's possible to receive extraneous onChange events even when no + // name has changed. + final Cursor cursor = mContext.getContentResolver().query(uri, + ContactsDictionaryConstants.PROJECTION, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + while (!cursor.isAfterLast()) { + final String name = cursor.getString( + ContactsDictionaryConstants.NAME_INDEX); + if (isValidName(name)) { + names.add(name); + } + cursor.moveToNext(); + } + } + } finally { + cursor.close(); + } + } + return names; + } + + /** + * Returns the number of contacts in contacts content provider. + */ + public int getContactCount() { + // TODO: consider switching to a rawQuery("select count(*)...") on the database if + // performance is a bottleneck. + Cursor cursor = null; + try { + cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI, + ContactsDictionaryConstants.PROJECTION_ID_ONLY, null, null, null); + if (null == cursor) { + return 0; + } + return cursor.getCount(); + } catch (final SQLiteException e) { + Log.e(TAG, "SQLiteException in the remote Contacts process.", e); + } finally { + if (null != cursor) { + cursor.close(); + } + } + return 0; + } + + private static boolean isValidName(final String name) { + if (name != null && -1 == name.indexOf(Constants.CODE_COMMERCIAL_AT)) { + return true; + } + return false; + } + + /** + * Updates the local state of the manager. This should be called when the callers + * are done with all the updates of the content provider successfully. + */ + public void updateLocalState(final ArrayList<String> names) { + mContactCountAtLastRebuild.set(getContactCount()); + mHashCodeAtLastRebuild.set(names.hashCode()); + } + + /** + * Performs any necessary cleanup. + */ + public void close() { + mObserver.unregister(); + } +} diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java index b341f623e..6816f129a 100644 --- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java +++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java @@ -16,7 +16,8 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.latin.settings.NativeSuggestOptions; +import com.android.inputmethod.latin.common.NativeSuggestOptions; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.utils.JniUtils; import java.util.Locale; @@ -27,20 +28,21 @@ public final class DicTraverseSession { } // Must be equal to MAX_RESULTS in native/jni/src/defines.h private static final int MAX_RESULTS = 18; - public final int[] mInputCodePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH]; + public final int[] mInputCodePoints = + new int[DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH]; public final int[][] mPrevWordCodePointArrays = - new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][]; + new int[DecoderSpecificConstants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][]; public final boolean[] mIsBeginningOfSentenceArray = - new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + new boolean[DecoderSpecificConstants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; public final int[] mOutputSuggestionCount = new int[1]; public final int[] mOutputCodePoints = - new int[Constants.DICTIONARY_MAX_WORD_LENGTH * MAX_RESULTS]; + new int[DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH * MAX_RESULTS]; public final int[] mSpaceIndices = new int[MAX_RESULTS]; public final int[] mOutputScores = new int[MAX_RESULTS]; 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 +72,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..16dcb3208 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,25 +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 = - new PhonyDictionary(TYPE_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 // phony dictionary instances for them. @@ -57,33 +59,44 @@ public abstract class Dictionary { public static final String TYPE_USER = "user"; // User history dictionary internal to LatinIME. public static final String TYPE_USER_HISTORY = "history"; - // Personalization dictionary. - public static final String TYPE_PERSONALIZATION = "personalization"; - // 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)); + + 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 +113,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 +177,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..d5dff10db 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -17,644 +17,160 @@ package com.android.inputmethod.latin; import android.content.Context; -import android.text.TextUtils; -import android.util.Log; -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.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.personalization.ContextualDictionary; -import com.android.inputmethod.latin.personalization.PersonalizationDataChunk; -import com.android.inputmethod.latin.personalization.PersonalizationDictionary; -import com.android.inputmethod.latin.personalization.UserHistoryDictionary; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.latin.common.ComposedData; 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.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; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -// TODO: Consolidate dictionaries in native code. -public class DictionaryFacilitator { - public static final String TAG = DictionaryFacilitator.class.getSimpleName(); - - // HACK: This threshold is being used when adding a capitalized entry in the User History - // dictionary. - private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140; - - private Dictionaries mDictionaries = new Dictionaries(); - private boolean mIsUserDictEnabled = false; - private volatile CountDownLatch mLatchForWaitingLoadingMainDictionary = new CountDownLatch(0); - // To synchronize assigning mDictionaries to ensure closing dictionaries. - private final Object mLock = new Object(); - private final DistracterFilter mDistracterFilter; - - private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS = - new String[] { - Dictionary.TYPE_MAIN, - Dictionary.TYPE_USER_HISTORY, - Dictionary.TYPE_PERSONALIZATION, - Dictionary.TYPE_USER, - Dictionary.TYPE_CONTACTS, - Dictionary.TYPE_CONTEXTUAL - }; - - public static final Map<String, Class<? extends ExpandableBinaryDictionary>> - DICT_TYPE_TO_CLASS = new HashMap<>(); - - static { - DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER_HISTORY, UserHistoryDictionary.class); - DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_PERSONALIZATION, PersonalizationDictionary.class); - DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER, UserBinaryDictionary.class); - DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTACTS, ContactsBinaryDictionary.class); - DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTEXTUAL, ContextualDictionary.class); - } +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Interface that facilitates interaction with different kinds of dictionaries. Provides APIs to + * instantiate and select the correct dictionaries (based on language or account), update entries + * and fetch suggestions. Currently AndroidSpellCheckerService and LatinIME both use + * DictionaryFacilitator as a client for interacting with dictionaries. + */ +public interface DictionaryFacilitator { - private static final String DICT_FACTORY_METHOD_NAME = "getDictionary"; - private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES = - new Class[] { Context.class, Locale.class, File.class, String.class }; + public static final String[] ALL_DICTIONARY_TYPES = new String[] { + Dictionary.TYPE_MAIN, + Dictionary.TYPE_USER_HISTORY, + Dictionary.TYPE_USER, + Dictionary.TYPE_CONTACTS}; - private static final String[] SUB_DICT_TYPES = - Arrays.copyOfRange(DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS, 1 /* start */, - DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS.length); + public static final String[] DYNAMIC_DICTIONARY_TYPES = new String[] { + Dictionary.TYPE_USER_HISTORY, + Dictionary.TYPE_USER, + Dictionary.TYPE_CONTACTS}; /** - * Class contains dictionaries for a locale. + * {@link Dictionary#TYPE_USER} is deprecated, except for the spelling service. */ - private static class Dictionaries { - public final Locale mLocale; - private Dictionary mMainDict; - public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap = - new ConcurrentHashMap<>(); - - public Dictionaries() { - mLocale = null; - } - - public Dictionaries(final Locale locale, final Dictionary mainDict, - final Map<String, ExpandableBinaryDictionary> subDicts) { - mLocale = locale; - // Main dictionary can be asynchronously loaded. - setMainDict(mainDict); - for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) { - setSubDict(entry.getKey(), entry.getValue()); - } - } - - private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) { - if (dict != null) { - mSubDictMap.put(dictType, dict); - } - } - - public void setMainDict(final Dictionary mainDict) { - // Close old dictionary if exists. Main dictionary can be assigned multiple times. - final Dictionary oldDict = mMainDict; - mMainDict = mainDict; - if (oldDict != null && mainDict != oldDict) { - oldDict.close(); - } - } - - public Dictionary getDict(final String dictType) { - if (Dictionary.TYPE_MAIN.equals(dictType)) { - return mMainDict; - } else { - return getSubDict(dictType); - } - } - - public ExpandableBinaryDictionary getSubDict(final String dictType) { - return mSubDictMap.get(dictType); - } - - public boolean hasDict(final String dictType) { - if (Dictionary.TYPE_MAIN.equals(dictType)) { - return mMainDict != null; - } else { - return mSubDictMap.containsKey(dictType); - } - } - - public void closeDict(final String dictType) { - final Dictionary dict; - if (Dictionary.TYPE_MAIN.equals(dictType)) { - dict = mMainDict; - } else { - dict = mSubDictMap.remove(dictType); - } - if (dict != null) { - dict.close(); - } - } - } + public static final String[] DICTIONARY_TYPES_FOR_SPELLING = new String[] { + Dictionary.TYPE_MAIN, + Dictionary.TYPE_USER_HISTORY, + Dictionary.TYPE_USER, + Dictionary.TYPE_CONTACTS}; - public interface DictionaryInitializationListener { - public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); - } + /** + * {@link Dictionary#TYPE_USER} is deprecated, except for the spelling service. + */ + public static final String[] DICTIONARY_TYPES_FOR_SUGGESTIONS = new String[] { + Dictionary.TYPE_MAIN, + Dictionary.TYPE_USER_HISTORY, + Dictionary.TYPE_CONTACTS}; - public DictionaryFacilitator() { - mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER; - } + /** + * Returns whether this facilitator is exactly for this locale. + * + * @param locale the locale to test against + */ + boolean isForLocale(final Locale locale); - public DictionaryFacilitator(final DistracterFilter distracterFilter) { - mDistracterFilter = distracterFilter; - } + /** + * Returns whether this facilitator is exactly for this account. + * + * @param account the account to test against. + */ + boolean isForAccount(@Nullable final String account); - public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { - mDistracterFilter.updateEnabledSubtypes(enabledSubtypes); + interface DictionaryInitializationListener { + void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); } - public Locale getLocale() { - return mDictionaries.mLocale; - } + /** + * Called every time {@link LatinIME} starts on a new text field. + * Dot not affect {@link AndroidSpellCheckerService}. + * + * WARNING: The service methods that call start/finish are very spammy. + */ + void onStartInput(); - private static ExpandableBinaryDictionary getSubDict(final String dictType, - final Context context, final Locale locale, final File dictFile, - final String dictNamePrefix) { - final Class<? extends ExpandableBinaryDictionary> dictClass = - DICT_TYPE_TO_CLASS.get(dictType); - if (dictClass == null) { - return null; - } - try { - final Method factoryMethod = dictClass.getMethod(DICT_FACTORY_METHOD_NAME, - DICT_FACTORY_METHOD_ARG_TYPES); - final Object dict = factoryMethod.invoke(null /* obj */, - new Object[] { context, locale, dictFile, dictNamePrefix }); - return (ExpandableBinaryDictionary) dict; - } catch (final NoSuchMethodException | SecurityException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException e) { - Log.e(TAG, "Cannot create dictionary: " + dictType, e); - return null; - } - } + /** + * Called every time the {@link LatinIME} finishes with the current text field. + * May be followed by {@link #onStartInput} again in another text field, + * or it may be done for a while. + * Dot not affect {@link AndroidSpellCheckerService}. + * + * WARNING: The service methods that call start/finish are very spammy. + */ + void onFinishInput(); - public void resetDictionaries(final Context context, final Locale newLocale, - final boolean useContactsDict, final boolean usePersonalizedDicts, - final boolean forceReloadMainDictionary, - final DictionaryInitializationListener listener) { - resetDictionariesWithDictNamePrefix(context, newLocale, useContactsDict, - usePersonalizedDicts, forceReloadMainDictionary, listener, "" /* dictNamePrefix */); - } + boolean isActive(); - public void resetDictionariesWithDictNamePrefix(final Context context, final Locale newLocale, - final boolean useContactsDict, final boolean usePersonalizedDicts, - final boolean forceReloadMainDictionary, - 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; - // TODO: Make subDictTypesToUse configurable by resource or a static final list. - final HashSet<String> subDictTypesToUse = new HashSet<>(); - 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. - continue; - } - final ExpandableBinaryDictionary dict; - if (!localeHasBeenChanged && mDictionaries.hasDict(dictType)) { - // Continue to use current dictionary. - dict = mDictionaries.getSubDict(dictType); - } else { - // Start to use new dictionary. - dict = getSubDict(dictType, context, newLocale, null /* dictFile */, - dictNamePrefix); - } - subDicts.put(dictType, dict); - } - - // Replace Dictionaries. - final Dictionaries newDictionaries = new Dictionaries(newLocale, newMainDict, subDicts); - final Dictionaries oldDictionaries; - synchronized (mLock) { - oldDictionaries = mDictionaries; - mDictionaries = newDictionaries; - mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context); - if (reloadMainDictionary) { - asyncReloadMainDictionary(context, newLocale, listener); - } - } - if (listener != null) { - listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary()); - } - // 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); - } - } - oldDictionaries.mSubDictMap.clear(); - } + Locale getLocale(); - private void asyncReloadMainDictionary(final Context context, final Locale locale, - final DictionaryInitializationListener listener) { - final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); - mLatchForWaitingLoadingMainDictionary = 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(); - } - }); - } + void resetDictionaries( + final Context context, + final Locale newLocale, + final boolean useContactsDict, + final boolean usePersonalizedDicts, + final boolean forceReloadMainDictionary, + @Nullable final String account, + final String dictNamePrefix, + @Nullable final DictionaryInitializationListener listener); @UsedForTesting - public void resetDictionariesForTesting(final Context context, final Locale locale, - 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); - } - dict.reloadDictionaryIfRequired(); - dict.waitAllTasksForTests(); - subDicts.put(dictType, dict); - } - } - mDictionaries = new Dictionaries(locale, mainDictionary, subDicts); - } + void resetDictionariesForTesting( + final Context context, + final Locale locale, + final ArrayList<String> dictionaryTypes, + final HashMap<String, File> dictionaryFiles, + final Map<String, Map<String, String>> additionalDictAttributes, + @Nullable final String account); - public void closeDictionaries() { - final Dictionaries dictionaries; - synchronized (mLock) { - dictionaries = mDictionaries; - mDictionaries = new Dictionaries(); - } - for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - dictionaries.closeDict(dictType); - } - mDistracterFilter.close(); - } + void closeDictionaries(); @UsedForTesting - public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { - return mDictionaries.getSubDict(dictName); - } + ExpandableBinaryDictionary getSubDictForTesting(final String 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. + boolean hasAtLeastOneInitializedMainDictionary(); - public boolean hasPersonalizationDictionary() { - return mDictionaries.hasDict(Dictionary.TYPE_PERSONALIZATION); - } + boolean hasAtLeastOneUninitializedMainDictionary(); - public void flushPersonalizationDictionary() { - final ExpandableBinaryDictionary personalizationDict = - mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION); - if (personalizationDict != null) { - personalizationDict.asyncFlushBinaryDictionary(); - } - } - - public void waitForLoadingMainDictionary(final long timeout, final TimeUnit unit) - throws InterruptedException { - mLatchForWaitingLoadingMainDictionary.await(timeout, unit); - } + void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit) + throws InterruptedException; @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(); - } - } - - public boolean isUserDictionaryEnabled() { - return mIsUserDictEnabled; - } + void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) + throws InterruptedException; - public void addWordToUserDictionary(final Context context, final String word) { - final Locale locale = getLocale(); - if (locale == null) { - return; - } - UserBinaryDictionary.addWordToUserDictionary(context, locale, word); - } + void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, + @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, + final boolean blockPotentiallyOffensive); - public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, - final PrevWordsInfo prevWordsInfo, final int timeStampInSeconds, - final boolean blockPotentiallyOffensive) { - final Dictionaries dictionaries = mDictionaries; - final String[] words = suggestion.split(Constants.WORD_SEPARATOR); - PrevWordsInfo prevWordsInfoForCurrentWord = prevWordsInfo; - for (int i = 0; i < words.length; i++) { - final String currentWord = words[i]; - final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false; - addWordToUserHistory(dictionaries, prevWordsInfoForCurrentWord, currentWord, - wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive); - prevWordsInfoForCurrentWord = - prevWordsInfoForCurrentWord.getNextPrevWordsInfo(new WordInfo(currentWord)); - } - } - - private void addWordToUserHistory(final Dictionaries dictionaries, - final PrevWordsInfo prevWordsInfo, final String word, final boolean wasAutoCapitalized, - final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { - final ExpandableBinaryDictionary userHistoryDictionary = - dictionaries.getSubDict(Dictionary.TYPE_USER_HISTORY); - if (userHistoryDictionary == null) { - return; - } - final int maxFreq = getFrequency(word); - if (maxFreq == 0 && blockPotentiallyOffensive) { - return; - } - final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); - final String secondWord; - if (wasAutoCapitalized) { - if (isValidWord(word, false /* ignoreCase */) - && !isValidWord(lowerCasedWord, false /* ignoreCase */)) { - // If the word was auto-capitalized and exists only as a capitalized word in the - // dictionary, then we must not downcase it before registering it. For example, - // the name of the contacts in start-of-sentence position would come here with the - // wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version - // of that contact's name which would end up popping in suggestions. - secondWord = word; - } else { - // If however the word is not in the dictionary, or exists as a lower-case word - // only, then we consider that was a lower-case word that had been auto-capitalized. - secondWord = lowerCasedWord; - } - } else { - // HACK: We'd like to avoid adding the capitalized form of common words to the User - // 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) : - Dictionary.NOT_A_PROBABILITY; - if (maxFreq < lowerCaseFreqInMainDict - && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) { - // Use lower cased word as the word can be a distracter of the popular word. - secondWord = lowerCasedWord; - } else { - secondWord = word; - } - } - // 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, - isValid, timeStampInSeconds, - new DistracterFilterCheckingIsInDictionary( - mDistracterFilter, userHistoryDictionary)); - } - - private void removeWord(final String dictName, final String word) { - final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); - if (dictionary != null) { - dictionary.removeUnigramEntryDynamically(word); - } - } - - public void removeWordFromPersonalizedDicts(final String word) { - removeWord(Dictionary.TYPE_USER_HISTORY, word); - removeWord(Dictionary.TYPE_PERSONALIZATION, word); - removeWord(Dictionary.TYPE_CONTEXTUAL, word); - } + void unlearnFromUserHistory(final String word, + @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, + final int eventType); // TODO: Revise the way to fusion suggestion results. - public SuggestionResults getSuggestionResults(final WordComposer composer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, - final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) { - final Dictionaries dictionaries = mDictionaries; - 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); - } - } - return suggestionResults; - } - - public boolean isValidWord(final String word, final boolean ignoreCase) { - 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; - } - } - return false; - } - - private int getFrequencyInternal(final String word, - final boolean isGettingMaxFrequencyOfExactMatches) { - if (TextUtils.isEmpty(word)) { - 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; - } - } - return maxFreq; - } + @Nonnull SuggestionResults getSuggestionResults(final ComposedData composedData, + final NgramContext ngramContext, @Nonnull final Keyboard keyboard, + final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId, + final int inputStyle); - public int getFrequency(final String word) { - return getFrequencyInternal(word, false /* isGettingMaxFrequencyOfExactMatches */); - } + boolean isValidSpellingWord(final String word); - public int getMaxFrequencyOfExactMatches(final String word) { - return getFrequencyInternal(word, true /* isGettingMaxFrequencyOfExactMatches */); - } + boolean isValidSuggestionWord(final String word); - private void clearSubDictionary(final String dictName) { - final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); - if (dictionary != null) { - dictionary.clear(); - } - } + void clearUserHistoryDictionary(final Context context); - public void clearUserHistoryDictionary() { - clearSubDictionary(Dictionary.TYPE_USER_HISTORY); - } + String dump(final Context context); - // This method gets called only when the IME receives a notification to remove the - // personalization dictionary. - public void clearPersonalizationDictionary() { - clearSubDictionary(Dictionary.TYPE_PERSONALIZATION); - } + void dumpDictionaryForDebug(final String dictName); - public void clearContextualDictionary() { - clearSubDictionary(Dictionary.TYPE_CONTEXTUAL); - } - - 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); - } - - public void addPhraseToContextualDictionary(final String[] phrase, final int probability, - final int bigramProbabilityForWords, final int bigramProbabilityForPhrases) { - final ExpandableBinaryDictionary contextualDict = - mDictionaries.getSubDict(Dictionary.TYPE_CONTEXTUAL); - if (contextualDict == null) { - return; - } - PrevWordsInfo prevWordsInfo = PrevWordsInfo.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 */, - BinaryDictionary.NOT_A_VALID_TIMESTAMP, - DistracterFilter.EMPTY_DISTRACTER_FILTER); - contextualDict.addNgramEntry(prevWordsInfo, 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 */, - BinaryDictionary.NOT_A_VALID_TIMESTAMP, - DistracterFilter.EMPTY_DISTRACTER_FILTER); - contextualDict.addNgramEntry(prevWordsInfo, phrase[i], - bigramProbabilityForWords, BinaryDictionary.NOT_A_VALID_TIMESTAMP); - } - prevWordsInfo = - prevWordsInfo.getNextPrevWordsInfo(new PrevWordsInfo.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; - } - dictToDump.dumpAllWordsForDebug(); - } + @Nonnull List<DictionaryStats> getDictionaryStats(final Context context); } diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java new file mode 100644 index 000000000..9ce92da9e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java @@ -0,0 +1,661 @@ +/* +7 * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.latin.NgramContext.WordInfo; +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.personalization.UserHistoryDictionary; +import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; +import com.android.inputmethod.latin.utils.ExecutorUtils; +import com.android.inputmethod.latin.utils.SuggestionResults; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Facilitates interaction with different kinds of dictionaries. Provides APIs + * to instantiate and select the correct dictionaries (based on language or account), + * update entries and fetch suggestions. + * + * Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as + * a client for interacting with dictionaries. + */ +public class DictionaryFacilitatorImpl implements DictionaryFacilitator { + // TODO: Consolidate dictionaries in native code. + public static final String TAG = DictionaryFacilitatorImpl.class.getSimpleName(); + + // HACK: This threshold is being used when adding a capitalized entry in the User History + // dictionary. + private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140; + + private DictionaryGroup mDictionaryGroup = new DictionaryGroup(); + private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0); + // To synchronize assigning mDictionaryGroup to ensure closing dictionaries. + private final Object mLock = new Object(); + + public static final Map<String, Class<? extends ExpandableBinaryDictionary>> + DICT_TYPE_TO_CLASS = new HashMap<>(); + + static { + DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER_HISTORY, UserHistoryDictionary.class); + DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER, UserBinaryDictionary.class); + DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTACTS, ContactsBinaryDictionary.class); + } + + private static final String DICT_FACTORY_METHOD_NAME = "getDictionary"; + private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES = + new Class[] { Context.class, Locale.class, File.class, String.class, String.class }; + + @Override + public boolean isForLocale(final Locale locale) { + return locale != null && locale.equals(mDictionaryGroup.mLocale); + } + + /** + * Returns whether this facilitator is exactly for this account. + * + * @param account the account to test against. + */ + public boolean isForAccount(@Nullable final String account) { + return TextUtils.equals(mDictionaryGroup.mAccount, account); + } + + /** + * A group of dictionaries that work together for a single language. + */ + private static class DictionaryGroup { + // TODO: Add null analysis annotations. + // 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; + + /** + * The locale associated with the dictionary group. + */ + @Nullable public final Locale mLocale; + + /** + * The user account associated with the dictionary group. + */ + @Nullable public final String mAccount; + + @Nullable private Dictionary mMainDict; + // Confidence that the most probable language is actually the language the user is + // typing in. For now, this is simply the number of times a word from this language + // has been committed in a row. + private int mConfidence = 0; + + 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 DictionaryGroup() { + this(null /* locale */, null /* mainDict */, null /* account */, + Collections.<String, ExpandableBinaryDictionary>emptyMap() /* subDicts */); + } + + public DictionaryGroup(@Nullable final Locale locale, + @Nullable final Dictionary mainDict, + @Nullable final String account, + final Map<String, ExpandableBinaryDictionary> subDicts) { + mLocale = locale; + mAccount = account; + // The main dictionary can be asynchronously loaded. + setMainDict(mainDict); + for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) { + setSubDict(entry.getKey(), entry.getValue()); + } + } + + private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) { + if (dict != null) { + mSubDictMap.put(dictType, dict); + } + } + + public void setMainDict(final Dictionary mainDict) { + // Close old dictionary if exists. Main dictionary can be assigned multiple times. + final Dictionary oldDict = mMainDict; + mMainDict = mainDict; + if (oldDict != null && mainDict != oldDict) { + oldDict.close(); + } + } + + public Dictionary getDict(final String dictType) { + if (Dictionary.TYPE_MAIN.equals(dictType)) { + return mMainDict; + } + return getSubDict(dictType); + } + + public ExpandableBinaryDictionary getSubDict(final String dictType) { + return mSubDictMap.get(dictType); + } + + public boolean hasDict(final String dictType, @Nullable final String account) { + if (Dictionary.TYPE_MAIN.equals(dictType)) { + return mMainDict != null; + } + if (Dictionary.TYPE_USER_HISTORY.equals(dictType) && + !TextUtils.equals(account, mAccount)) { + // If the dictionary type is user history, & if the account doesn't match, + // return immediately. If the account matches, continue looking it up in the + // sub dictionary map. + return false; + } + return mSubDictMap.containsKey(dictType); + } + + public void closeDict(final String dictType) { + final Dictionary dict; + if (Dictionary.TYPE_MAIN.equals(dictType)) { + dict = mMainDict; + } else { + dict = mSubDictMap.remove(dictType); + } + if (dict != null) { + dict.close(); + } + } + } + + public DictionaryFacilitatorImpl() { + } + + @Override + public void onStartInput() { + } + + @Override + public void onFinishInput() { + } + + @Override + public boolean isActive() { + return mDictionaryGroup.mLocale != null; + } + + @Override + public Locale getLocale() { + return mDictionaryGroup.mLocale; + } + + @Nullable + private static ExpandableBinaryDictionary getSubDict(final String dictType, + final Context context, final Locale locale, final File dictFile, + final String dictNamePrefix, @Nullable final String account) { + final Class<? extends ExpandableBinaryDictionary> dictClass = + DICT_TYPE_TO_CLASS.get(dictType); + if (dictClass == null) { + return null; + } + try { + final Method factoryMethod = dictClass.getMethod(DICT_FACTORY_METHOD_NAME, + DICT_FACTORY_METHOD_ARG_TYPES); + final Object dict = factoryMethod.invoke(null /* obj */, + new Object[] { context, locale, dictFile, dictNamePrefix, account }); + return (ExpandableBinaryDictionary) dict; + } catch (final NoSuchMethodException | SecurityException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException e) { + Log.e(TAG, "Cannot create dictionary: " + dictType, e); + return null; + } + } + + @Nullable + static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup dictionaryGroup, + final Locale locale) { + return locale.equals(dictionaryGroup.mLocale) ? dictionaryGroup : null; + } + + @Override + public void resetDictionaries( + final Context context, + final Locale newLocale, + final boolean useContactsDict, + final boolean usePersonalizedDicts, + final boolean forceReloadMainDictionary, + @Nullable final String account, + final String dictNamePrefix, + @Nullable final DictionaryInitializationListener listener) { + final HashMap<Locale, ArrayList<String>> existingDictionariesToCleanup = 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); + } + if (usePersonalizedDicts) { + subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); + } + + // Gather all dictionaries. We'll remove them from the list to clean up later. + final ArrayList<String> dictTypeForLocale = new ArrayList<>(); + existingDictionariesToCleanup.put(newLocale, dictTypeForLocale); + final DictionaryGroup currentDictionaryGroupForLocale = + findDictionaryGroupWithLocale(mDictionaryGroup, newLocale); + if (currentDictionaryGroupForLocale != null) { + for (final String dictType : DYNAMIC_DICTIONARY_TYPES) { + if (currentDictionaryGroupForLocale.hasDict(dictType, account)) { + dictTypeForLocale.add(dictType); + } + } + if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) { + dictTypeForLocale.add(Dictionary.TYPE_MAIN); + } + } + + final DictionaryGroup dictionaryGroupForLocale = + findDictionaryGroupWithLocale(mDictionaryGroup, newLocale); + final ArrayList<String> dictTypesToCleanupForLocale = + existingDictionariesToCleanup.get(newLocale); + final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale); + + final Dictionary mainDict; + if (forceReloadMainDictionary || noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) { + mainDict = null; + } else { + mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN); + dictTypesToCleanupForLocale.remove(Dictionary.TYPE_MAIN); + } + + final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); + for (final String subDictType : subDictTypesToUse) { + final ExpandableBinaryDictionary subDict; + if (noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(subDictType, account)) { + // Create a new dictionary. + subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */, + dictNamePrefix, account); + } else { + // Reuse the existing dictionary, and don't close it at the end + subDict = dictionaryGroupForLocale.getSubDict(subDictType); + dictTypesToCleanupForLocale.remove(subDictType); + } + subDicts.put(subDictType, subDict); + } + DictionaryGroup newDictionaryGroup = + new DictionaryGroup(newLocale, mainDict, account, subDicts); + + // Replace Dictionaries. + final DictionaryGroup oldDictionaryGroup; + synchronized (mLock) { + oldDictionaryGroup = mDictionaryGroup; + mDictionaryGroup = newDictionaryGroup; + if (hasAtLeastOneUninitializedMainDictionary()) { + asyncReloadUninitializedMainDictionaries(context, newLocale, listener); + } + } + if (listener != null) { + listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary()); + } + + // Clean up old dictionaries. + for (final Locale localeToCleanUp : existingDictionariesToCleanup.keySet()) { + final ArrayList<String> dictTypesToCleanUp = + existingDictionariesToCleanup.get(localeToCleanUp); + final DictionaryGroup dictionarySetToCleanup = + findDictionaryGroupWithLocale(oldDictionaryGroup, localeToCleanUp); + for (final String dictType : dictTypesToCleanUp) { + dictionarySetToCleanup.closeDict(dictType); + } + } + } + + private void asyncReloadUninitializedMainDictionaries(final Context context, + final Locale locale, final DictionaryInitializationListener listener) { + final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); + mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary; + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() { + @Override + public void run() { + doReloadUninitializedMainDictionaries( + context, locale, listener, latchForWaitingLoadingMainDictionary); + } + }); + } + + void doReloadUninitializedMainDictionaries(final Context context, final Locale locale, + final DictionaryInitializationListener listener, + final CountDownLatch latchForWaitingLoadingMainDictionary) { + final DictionaryGroup dictionaryGroup = + findDictionaryGroupWithLocale(mDictionaryGroup, 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"); + return; + } + 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, + final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles, + final Map<String, Map<String, String>> additionalDictAttributes, + @Nullable final String account) { + 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 */, account); + 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); + } + } + mDictionaryGroup = new DictionaryGroup(locale, mainDictionary, account, subDicts); + } + + public void closeDictionaries() { + final DictionaryGroup dictionaryGroupToClose; + synchronized (mLock) { + dictionaryGroupToClose = mDictionaryGroup; + mDictionaryGroup = new DictionaryGroup(); + } + for (final String dictType : ALL_DICTIONARY_TYPES) { + dictionaryGroupToClose.closeDict(dictType); + } + } + + @UsedForTesting + public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { + return mDictionaryGroup.getSubDict(dictName); + } + + // The main dictionaries are loaded asynchronously. Don't cache the return value + // of these methods. + public boolean hasAtLeastOneInitializedMainDictionary() { + final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN); + if (mainDict != null && mainDict.isInitialized()) { + return true; + } + return false; + } + + public boolean hasAtLeastOneUninitializedMainDictionary() { + final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN); + if (mainDict == null || !mainDict.isInitialized()) { + return true; + } + return false; + } + + public void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit) + throws InterruptedException { + mLatchForWaitingLoadingMainDictionaries.await(timeout, unit); + } + + @UsedForTesting + public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) + throws InterruptedException { + waitForLoadingMainDictionaries(timeout, unit); + for (final ExpandableBinaryDictionary dict : mDictionaryGroup.mSubDictMap.values()) { + dict.waitAllTasksForTests(); + } + } + + public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, + @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, + final boolean blockPotentiallyOffensive) { + final String[] words = suggestion.split(Constants.WORD_SEPARATOR); + NgramContext ngramContextForCurrentWord = ngramContext; + for (int i = 0; i < words.length; i++) { + final String currentWord = words[i]; + final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false; + addWordToUserHistory(mDictionaryGroup, ngramContextForCurrentWord, currentWord, + wasCurrentWordAutoCapitalized, (int) timeStampInSeconds, + blockPotentiallyOffensive); + ngramContextForCurrentWord = + ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord)); + } + } + + private void addWordToUserHistory(final DictionaryGroup dictionaryGroup, + final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized, + final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { + final ExpandableBinaryDictionary userHistoryDictionary = + dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY); + if (userHistoryDictionary == null || !isForLocale(userHistoryDictionary.mLocale)) { + return; + } + final int maxFreq = getFrequency(word); + if (maxFreq == 0 && blockPotentiallyOffensive) { + return; + } + final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); + final String secondWord; + if (wasAutoCapitalized) { + if (isValidSuggestionWord(word) && !isValidSuggestionWord(lowerCasedWord)) { + // If the word was auto-capitalized and exists only as a capitalized word in the + // dictionary, then we must not downcase it before registering it. For example, + // the name of the contacts in start-of-sentence position would come here with the + // wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version + // of that contact's name which would end up popping in suggestions. + secondWord = word; + } else { + // If however the word is not in the dictionary, or exists as a lower-case word + // only, then we consider that was a lower-case word that had been auto-capitalized. + secondWord = lowerCasedWord; + } + } else { + // HACK: We'd like to avoid adding the capitalized form of common words to the User + // History dictionary in order to avoid suggesting them until the dictionary + // consolidation is done. + // TODO: Remove this hack when ready. + final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN, + null /* account */) ? + dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) : + Dictionary.NOT_A_PROBABILITY; + if (maxFreq < lowerCaseFreqInMainDict + && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) { + // Use lower cased word as the word can be a distracter of the popular word. + secondWord = lowerCasedWord; + } else { + secondWord = word; + } + } + // 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, ngramContext, secondWord, + isValid, timeStampInSeconds); + } + + private void removeWord(final String dictName, final String word) { + final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); + if (dictionary != null) { + dictionary.removeUnigramEntryDynamically(word); + } + } + + @Override + public void unlearnFromUserHistory(final String word, + @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, + final int eventType) { + // TODO: Decide whether or not to remove the word on EVENT_BACKSPACE. + if (eventType != Constants.EVENT_BACKSPACE) { + removeWord(Dictionary.TYPE_USER_HISTORY, word); + } + } + + // TODO: Revise the way to fusion suggestion results. + @Override + @Nonnull public SuggestionResults getSuggestionResults(ComposedData composedData, + NgramContext ngramContext, @Nonnull final Keyboard keyboard, + SettingsValuesForSuggestion settingsValuesForSuggestion, int sessionId, + int inputStyle) { + long proximityInfoHandle = keyboard.getProximityInfo().getNativeProximityInfo(); + final SuggestionResults suggestionResults = new SuggestionResults( + SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext(), + false /* firstSuggestionExceedsConfidenceThreshold */); + final float[] weightOfLangModelVsSpatialModel = + new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL }; + for (final String dictType : DICTIONARY_TYPES_FOR_SUGGESTIONS) { + final Dictionary dictionary = mDictionaryGroup.getDict(dictType); + if (null == dictionary) continue; + final float weightForLocale = composedData.mIsBatchMode + ? mDictionaryGroup.mWeightForGesturingInLocale + : mDictionaryGroup.mWeightForTypingInLocale; + final ArrayList<SuggestedWordInfo> dictionarySuggestions = + dictionary.getSuggestions(composedData, ngramContext, + proximityInfoHandle, settingsValuesForSuggestion, sessionId, + weightForLocale, weightOfLangModelVsSpatialModel); + if (null == dictionarySuggestions) continue; + suggestionResults.addAll(dictionarySuggestions); + if (null != suggestionResults.mRawSuggestions) { + suggestionResults.mRawSuggestions.addAll(dictionarySuggestions); + } + } + return suggestionResults; + } + + public boolean isValidSpellingWord(final String word) { + return isValidWord(word, DICTIONARY_TYPES_FOR_SPELLING); + } + + public boolean isValidSuggestionWord(final String word) { + return isValidWord(word, DICTIONARY_TYPES_FOR_SUGGESTIONS); + } + + private boolean isValidWord(final String word, final String[] dictionariesToCheck) { + if (TextUtils.isEmpty(word)) { + return false; + } + if (mDictionaryGroup.mLocale == null) { + return false; + } + for (final String dictType : dictionariesToCheck) { + final Dictionary dictionary = mDictionaryGroup.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)) { + return true; + } + } + return false; + } + + private int getFrequency(final String word) { + if (TextUtils.isEmpty(word)) { + return Dictionary.NOT_A_PROBABILITY; + } + int maxFreq = Dictionary.NOT_A_PROBABILITY; + for (final String dictType : ALL_DICTIONARY_TYPES) { + final Dictionary dictionary = mDictionaryGroup.getDict(dictType); + if (dictionary == null) continue; + final int tempFreq = dictionary.getFrequency(word); + if (tempFreq >= maxFreq) { + maxFreq = tempFreq; + } + } + return maxFreq; + } + + private void clearSubDictionary(final String dictName) { + final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); + if (dictionary != null) { + dictionary.clear(); + } + } + + @Override + public void clearUserHistoryDictionary(final Context context) { + clearSubDictionary(Dictionary.TYPE_USER_HISTORY); + } + + @Override + public void dumpDictionaryForDebug(final String dictName) { + final ExpandableBinaryDictionary dictToDump = mDictionaryGroup.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(); + } + + @Override + @Nonnull public List<DictionaryStats> getDictionaryStats(final Context context) { + final ArrayList<DictionaryStats> statsOfEnabledSubDicts = new ArrayList<>(); + for (final String dictType : DYNAMIC_DICTIONARY_TYPES) { + final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictType); + if (dictionary == null) continue; + statsOfEnabledSubDicts.add(dictionary.getDictionaryStats()); + } + return statsOfEnabledSubDicts; + } + + @Override + public String dump(final Context context) { + return ""; + } +} 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..cbaf6ea4e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java @@ -0,0 +1,106 @@ +/* + * 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.Locale; +import java.util.concurrent.TimeUnit; + +import android.content.Context; +import android.util.Log; + +/** + * Cache for dictionary facilitators of multiple locales. + * This class automatically creates and releases up to 3 facilitator instances using LRU policy. + */ +public class DictionaryFacilitatorLruCache { + private static final String TAG = "DictionaryFacilitatorLruCache"; + 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 final Context mContext; + private final String mDictionaryNamePrefix; + private final Object mLock = new Object(); + private final DictionaryFacilitator mDictionaryFacilitator; + private boolean mUseContactsDictionary; + private Locale mLocale; + + public DictionaryFacilitatorLruCache(final Context context, final String dictionaryNamePrefix) { + mContext = context; + mDictionaryNamePrefix = dictionaryNamePrefix; + mDictionaryFacilitator = DictionaryFacilitatorProvider.getDictionaryFacilitator( + true /* isNeededForSpellChecking */); + } + + 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() { + // Nothing to do if the locale is null. This would be the case before any get() calls. + if (mLocale != null) { + // Note: Given that personalized dictionaries are not used here; we can pass null account. + mDictionaryFacilitator.resetDictionaries(mContext, mLocale, + mUseContactsDictionary, false /* usePersonalizedDicts */, + false /* forceReloadMainDictionary */, null /* account */, + mDictionaryNamePrefix, null /* listener */); + } + } + + public void setUseContactsDictionary(final boolean useContactsDictionary) { + synchronized (mLock) { + if (mUseContactsDictionary == useContactsDictionary) { + // The value has not been changed. + return; + } + mUseContactsDictionary = useContactsDictionary; + resetDictionariesForLocaleLocked(); + waitForLoadingMainDictionary(mDictionaryFacilitator); + } + } + + public DictionaryFacilitator get(final Locale locale) { + synchronized (mLock) { + if (!mDictionaryFacilitator.isForLocale(locale)) { + mLocale = locale; + resetDictionariesForLocaleLocked(); + } + waitForLoadingMainDictionary(mDictionaryFacilitator); + return mDictionaryFacilitator; + } + } + + public void closeDictionaries() { + synchronized (mLock) { + mDictionaryFacilitator.closeDictionaries(); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java index 59de4f82a..49608d830 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java @@ -19,10 +19,8 @@ package com.android.inputmethod.latin; import android.content.ContentProviderClient; import android.content.Context; import android.content.res.AssetFileDescriptor; -import android.content.res.Resources; import android.util.Log; -import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import java.io.File; @@ -43,14 +41,13 @@ public final class DictionaryFactory { * locale. If none is found, it falls back to the built-in dictionary - if any. * @param context application context for reading resources * @param locale the locale for which to create the dictionary - * @param useFullEditDistance whether to use the full edit distance in suggestions * @return an initialized instance of DictionaryCollection */ public static DictionaryCollection createMainDictionaryFromManager(final Context context, - final Locale locale, final boolean useFullEditDistance) { + final Locale locale) { 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)); } @@ -61,7 +58,7 @@ public final class DictionaryFactory { for (final AssetFileAddress f : assetFileList) { final ReadOnlyBinaryDictionary readOnlyBinaryDictionary = new ReadOnlyBinaryDictionary(f.mFilename, f.mOffset, f.mLength, - useFullEditDistance, locale, Dictionary.TYPE_MAIN); + false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN); if (readOnlyBinaryDictionary.isValidDictionary()) { dictList.add(readOnlyBinaryDictionary); } else { @@ -75,7 +72,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); } /** @@ -83,7 +80,7 @@ public final class DictionaryFactory { * @param context The context to contact the dictionary provider, if possible. * @param f A file address to the dictionary to kill. */ - private static void killDictionary(final Context context, final AssetFileAddress f) { + public static void killDictionary(final Context context, final AssetFileAddress f) { if (f.pointsToPhysicalFile()) { f.deleteUnderlyingFile(); // Warn the dictionary provider if the dictionary came from there. @@ -101,49 +98,33 @@ public final class DictionaryFactory { } final String wordlistId = DictionaryInfoUtils.getWordListIdFromFileName(new File(f.mFilename).getName()); - if (null != wordlistId) { - // TODO: this is a reasonable last resort, but it is suboptimal. - // The following will remove the entry for this dictionary with the dictionary - // provider. When the metadata is downloaded again, we will try downloading it - // again. - // However, in the practice that will mean the user will find themselves without - // the new dictionary. That's fine for languages where it's included in the APK, - // but for other languages it will leave the user without a dictionary at all until - // the next update, which may be a few days away. - // Ideally, we would trigger a new download right away, and use increasing retry - // delays for this particular id/version combination. - // Then again, this is expected to only ever happen in case of human mistake. If - // the wrong file is on the server, the following is still doing the right thing. - // If it's a file left over from the last version however, it's not great. - BinaryDictionaryFileDumper.reportBrokenFileToDictionaryProvider( - providerClient, - context.getString(R.string.dictionary_pack_client_id), - wordlistId); - } + // TODO: this is a reasonable last resort, but it is suboptimal. + // The following will remove the entry for this dictionary with the dictionary + // provider. When the metadata is downloaded again, we will try downloading it + // again. + // However, in the practice that will mean the user will find themselves without + // the new dictionary. That's fine for languages where it's included in the APK, + // but for other languages it will leave the user without a dictionary at all until + // the next update, which may be a few days away. + // Ideally, we would trigger a new download right away, and use increasing retry + // delays for this particular id/version combination. + // Then again, this is expected to only ever happen in case of human mistake. If + // the wrong file is on the server, the following is still doing the right thing. + // If it's a file left over from the last version however, it's not great. + BinaryDictionaryFileDumper.reportBrokenFileToDictionaryProvider( + providerClient, + context.getString(R.string.dictionary_pack_client_id), + wordlistId); } } /** - * Initializes a main dictionary collection from a dictionary pack, with default flags. - * - * This searches for a content provider providing a dictionary pack for the specified - * locale. If none is found, it falls back to the built-in dictionary, if any. - * @param context application context for reading resources - * @param locale the locale for which to create the dictionary - * @return an initialized instance of DictionaryCollection - */ - public static DictionaryCollection createMainDictionaryFromManager(final Context context, - final Locale locale) { - return createMainDictionaryFromManager(context, locale, false /* useFullEditDistance */); - } - - /** * Initializes a read-only binary dictionary from a raw resource file * @param context application context for reading resources * @param locale the locale to use for the resource * @return an initialized instance of ReadOnlyBinaryDictionary */ - protected static ReadOnlyBinaryDictionary createReadOnlyBinaryDictionary(final Context context, + private static ReadOnlyBinaryDictionary createReadOnlyBinaryDictionary(final Context context, final Locale locale) { AssetFileDescriptor afd = null; try { @@ -177,36 +158,4 @@ public final class DictionaryFactory { } } } - - /** - * Create a dictionary from passed data. This is intended for unit tests only. - * @param dictionaryList the list of files to read, with their offsets and lengths - * @param useFullEditDistance whether to use the full edit distance in suggestions - * @return the created dictionary, or null. - */ - @UsedForTesting - public static Dictionary createDictionaryForTest(final AssetFileAddress[] dictionaryList, - final boolean useFullEditDistance, Locale locale) { - final DictionaryCollection dictionaryCollection = - new DictionaryCollection(Dictionary.TYPE_MAIN); - for (final AssetFileAddress address : dictionaryList) { - final ReadOnlyBinaryDictionary readOnlyBinaryDictionary = new ReadOnlyBinaryDictionary( - address.mFilename, address.mOffset, address.mLength, useFullEditDistance, - locale, Dictionary.TYPE_MAIN); - dictionaryCollection.addDictionary(readOnlyBinaryDictionary); - } - return dictionaryCollection; - } - - /** - * Find out whether a dictionary is available for this locale. - * @param context the context on which to check resources. - * @param locale the locale to check for. - * @return whether a (non-placeholder) dictionary is available or not. - */ - public static boolean isDictionaryAvailable(Context context, Locale locale) { - final Resources res = context.getResources(); - return 0 != DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale( - res, locale); - } } 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..a6b37aa8f --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryStats.java @@ -0,0 +1,80 @@ +/* + * 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.math.BigDecimal; +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 mContentVersion; + + public DictionaryStats(final Locale locale, final String dictName, final File dictFile, + final int contentVersion) { + mLocale = locale; + mDictName = dictName; + mDictFilePath = (dictFile == null) ? null : dictFile.getName(); + mDictFileSize = (dictFile == null || !dictFile.exists()) ? 0 : dictFile.length(); + mContentVersion = contentVersion; + } + + public String getFileSizeString() { + if (mDictFileSize == 0) { + return "0"; + } + BigDecimal bytes = new BigDecimal(mDictFileSize); + BigDecimal kb = bytes.divide(new BigDecimal(1024), 2, BigDecimal.ROUND_HALF_UP); + if (kb.longValue() == 0) { + return bytes.toString() + " bytes"; + } + BigDecimal mb = kb.divide(new BigDecimal(1024), 2, BigDecimal.ROUND_HALF_UP); + if (mb.longValue() == 0) { + return kb.toString() + " kb"; + } + return mb.toString() + " Mb"; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(mDictName); + if (mDictName.equals(Dictionary.TYPE_MAIN)) { + builder.append(" ("); + builder.append(mContentVersion); + builder.append(")"); + } + builder.append(": "); + builder.append(mDictFilePath); + builder.append(" / "); + builder.append(getFileSizeString()); + return builder.toString(); + } + + public static String toString(final Iterable<DictionaryStats> stats) { + final StringBuilder builder = new StringBuilder("LM Stats"); + for (DictionaryStats stat : stats) { + builder.append("\n "); + builder.append(stat.toString()); + } + return builder.toString(); + } +} 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..9b271116d --- /dev/null +++ b/java/src/com/android/inputmethod/latin/EmojiAltPhysicalKeyDetector.java @@ -0,0 +1,107 @@ +/* + * 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.util.Log; +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 { + private static final String TAG = "EmojiAltPhysicalKeyDetector"; + + private final RichInputConnection mRichInputConnection; + + // 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; + + public EmojiAltPhysicalKeyDetector(final RichInputConnection richInputConnection) { + mRichInputConnection = richInputConnection; + } + + /** + * 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) { + return; + } + if (!mRichInputConnection.isConnected()) { + Log.w(TAG, "onKeyUp() : No connection to text view"); + return; + } + 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..1ef7061fb 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -20,35 +20,41 @@ 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.FileUtils; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; 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; import java.util.HashMap; import java.util.Locale; import java.util.Map; -import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; 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) */ abstract public class ExpandableBinaryDictionary extends Dictionary { private static final boolean DEBUG = false; @@ -61,16 +67,17 @@ 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. */ - protected static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; + protected static final int MAX_WORD_LENGTH = + DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH; private static final int DICTIONARY_FORMAT_VERSION = FormatSpec.VERSION4; + private static final WordProperty[] DEFAULT_WORD_PROPERTIES_FOR_SYNC = + new WordProperty[0] /* default */; + /** The application context. */ protected final Context mContext; @@ -86,9 +93,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 +114,14 @@ 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.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(); @@ -163,32 +166,10 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { asyncExecuteTaskWithLock(mLock.writeLock(), task); } - private void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) { - asyncPreCheckAndExecuteTaskWithLock(lock, null /* preCheckTask */, task); - } - - private void asyncPreCheckAndExecuteTaskWithWriteLock( - final Callable<Boolean> preCheckTask, final Runnable task) { - asyncPreCheckAndExecuteTaskWithLock(mLock.writeLock(), preCheckTask, 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 asyncExecuteTaskWithLock(final Lock lock, final Runnable task) { + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() { @Override public void run() { - if (preCheckTask != null) { - try { - if (!preCheckTask.call().booleanValue()) { - return; - } - } catch (final Exception e) { - Log.e(TAG, "The pre check task throws an exception.", e); - return; - } - } lock.lock(); try { task.run(); @@ -199,6 +180,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 +200,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { - if (mBinaryDictionary != null) { - mBinaryDictionary.close(); - mBinaryDictionary = null; - } + closeBinaryDictionary(); } }); } @@ -224,10 +214,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 +226,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 +239,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()); @@ -275,11 +258,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { /** * Check whether GC is needed and run GC if required. */ - protected void runGCIfRequired(final boolean mindsBlockByGC) { + public void runGCIfRequired(final boolean mindsBlockByGC) { asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { - if (mBinaryDictionary == null) { + if (getBinaryDictionary() == null) { return; } runGCIfRequiredLocked(mindsBlockByGC); @@ -293,40 +276,38 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } } + private void updateDictionaryWithWriteLock(@Nonnull final Runnable updateTask) { + reloadDictionaryIfRequired(); + final Runnable task = new Runnable() { + @Override + public void run() { + if (getBinaryDictionary() == null) { + return; + } + runGCIfRequiredLocked(true /* mindsBlockByGC */); + updateTask.run(); + } + }; + asyncExecuteTaskWithWriteLock(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); - } - }); + public void addUnigramEntry(final String word, final int frequency, + final boolean isNotAWord, final boolean isPossiblyOffensive, final int timestamp) { + updateDictionaryWithWriteLock(new Runnable() { + @Override + public void run() { + addUnigramLocked(word, frequency, isNotAWord, isPossiblyOffensive, timestamp); + } + }); } protected void addUnigramLocked(final String word, final int frequency, - final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, - final boolean isBlacklisted, final int timestamp) { - if (!mBinaryDictionary.addUnigramEntry(word, frequency, shortcutTarget, shortcutFreq, - false /* isBeginningOfSentence */, isNotAWord, isBlacklisted, timestamp)) { + final boolean isNotAWord, final boolean isPossiblyOffensive, final int timestamp) { + if (!mBinaryDictionary.addUnigramEntry(word, frequency, + false /* isBeginningOfSentence */, isNotAWord, isPossiblyOffensive, timestamp)) { Log.e(TAG, "Cannot add unigram entry. word: " + word); } } @@ -339,11 +320,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,75 +337,85 @@ 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); } } } /** - * Dynamically remove the n-gram entry in the dictionary. + * Update dictionary for the word with the ngramContext. */ - @UsedForTesting - public void removeNgramDynamically(final PrevWordsInfo prevWordsInfo, final String word) { - reloadDictionaryIfRequired(); - asyncExecuteTaskWithWriteLock(new Runnable() { + public void updateEntriesForWord(@Nonnull final NgramContext ngramContext, + final String word, final boolean isValidWord, final int count, final int timestamp) { + updateDictionaryWithWriteLock(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.updateEntriesForWordWithNgramContext(ngramContext, word, + isValidWord, count, timestamp)) { if (DEBUG) { - Log.i(TAG, "Cannot remove n-gram entry."); - Log.i(TAG, " PrevWordsInfo: " + prevWordsInfo + ", word: " + word); + Log.e(TAG, "Cannot update counter. word: " + word + + " context: " + ngramContext.toString()); } } } }); } - public interface AddMultipleDictionaryEntriesCallback { + /** + * Used by Sketch. + * {@see https://cs.corp.google.com/#android/vendor/unbundled_google/packages/LatinIMEGoogle/tools/sketch/ime-simulator/src/com/android/inputmethod/sketch/imesimulator/ImeSimulator.java&q=updateEntriesForInputEventsCallback&l=286} + */ + @UsedForTesting + public interface UpdateEntriesForInputEventsCallback { public void onFinished(); } /** - * Dynamically add multiple entries to the dictionary. + * Dynamically update entries according to input events. + * + * Used by Sketch. + * {@see https://cs.corp.google.com/#android/vendor/unbundled_google/packages/LatinIMEGoogle/tools/sketch/ime-simulator/src/com/android/inputmethod/sketch/imesimulator/ImeSimulator.java&q=updateEntriesForInputEventsCallback&l=286} */ - public void addMultipleDictionaryEntriesDynamically( - final ArrayList<LanguageModelParam> languageModelParams, - final AddMultipleDictionaryEntriesCallback callback) { + @UsedForTesting + 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 +426,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 +440,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 +512,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } - protected boolean isValidNgramLocked(final PrevWordsInfo prevWordsInfo, final String word) { - if (mBinaryDictionary == null) return false; - return mBinaryDictionary.isValidNgram(prevWordsInfo, 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. @@ -537,6 +525,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { Thread.sleep(15000); Log.w(TAG, "End stress in loading"); } catch (InterruptedException e) { + Log.w("Interrupted while loading: " + mDictName, e); } } final BinaryDictionary oldBinaryDictionary = mBinaryDictionary; @@ -556,7 +545,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 +561,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. @@ -593,36 +590,40 @@ 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. + private void asyncReloadDictionary() { + 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 +633,37 @@ 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(); } } }); } + public DictionaryStats getDictionaryStats() { + reloadDictionaryIfRequired(); + final String dictName = mDictName; + final File dictFile = mDictFile; + final AsyncResultHolder<DictionaryStats> result = new AsyncResultHolder<>(); + asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() { + @Override + public void run() { + result.set(new DictionaryStats(mLocale, dictName, dictFile, 0)); + } + }); + 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,31 +685,71 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { public void dumpAllWordsForDebug() { reloadDictionaryIfRequired(); + final String tag = TAG; + final String dictName = mDictName; asyncExecuteTaskWithLock(mLock.readLock(), 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); } }); } + + /** + * Returns dictionary content required for syncing. + */ + public WordProperty[] getWordPropertiesForSyncing() { + reloadDictionaryIfRequired(); + final AsyncResultHolder<WordProperty[]> result = new AsyncResultHolder<>(); + asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() { + @Override + public void run() { + final ArrayList<WordProperty> wordPropertyList = new ArrayList<>(); + final BinaryDictionary binaryDictionary = getBinaryDictionary(); + if (binaryDictionary == null) { + return; + } + int token = 0; + do { + // TODO: We need a new API that returns *new* un-synced data. + final BinaryDictionary.GetNextWordPropertyResult nextWordPropertyResult = + binaryDictionary.getNextWordProperty(token); + final WordProperty wordProperty = nextWordPropertyResult.mWordProperty; + if (wordProperty == null) { + break; + } + wordPropertyList.add(wordProperty); + token = nextWordPropertyResult.mNextToken; + } while (token != 0); + result.set(wordPropertyList.toArray(new WordProperty[wordPropertyList.size()])); + } + }); + // TODO: Figure out the best timeout duration for this API. + return result.get(DEFAULT_WORD_PROPERTIES_FOR_SYNC, + TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS); + } } 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..426d33e6d 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.InputPointers; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; import java.util.ArrayList; @@ -48,10 +50,10 @@ 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); + new InputPointers(DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH); private boolean mActive; @@ -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..330be377b 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -16,9 +16,9 @@ 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.app.AlertDialog; import android.content.BroadcastReceiver; @@ -31,13 +31,11 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.inputmethodservice.InputMethodService; import android.media.AudioManager; -import android.net.ConnectivityManager; import android.os.Debug; import android.os.IBinder; import android.os.Message; import android.preference.PreferenceManager; import android.text.InputType; -import android.text.TextUtils; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; @@ -46,19 +44,17 @@ import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; -import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; -import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; -import android.widget.TextView; 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; @@ -69,32 +65,29 @@ import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardSwitcher; 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.CoordinateUtils; +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; -import com.android.inputmethod.latin.personalization.ContextualDictionaryUpdater; -import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever; -import com.android.inputmethod.latin.personalization.PersonalizationDictionaryUpdater; import com.android.inputmethod.latin.personalization.PersonalizationHelper; import com.android.inputmethod.latin.settings.Settings; 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; @@ -105,6 +98,8 @@ 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 +107,14 @@ 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,22 +122,11 @@ 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 */)); - // TODO: Move from LatinIME. - private final PersonalizationDictionaryUpdater mPersonalizationDictionaryUpdater = - new PersonalizationDictionaryUpdater(this /* context */, mDictionaryFacilitator); - private final ContextualDictionaryUpdater mContextualDictionaryUpdater = - new ContextualDictionaryUpdater(this /* context */, mDictionaryFacilitator, - new Runnable() { - @Override - public void run() { - mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE); - } - }); - private final InputLogic mInputLogic = new InputLogic(this /* LatinIME */, + DictionaryFacilitatorProvider.getDictionaryFacilitator( + false /* isNeededForSpellChecking */); + 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 +134,15 @@ 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; private final SubtypeState mSubtypeState = new SubtypeState(); - private final SpecialKeyDetector mSpecialKeyDetector; + private final EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector = + new EmojiAltPhysicalKeyDetector(mInputLogic.mConnection); + private StatsUtilsManager mStatsUtilsManager; // Working variable for {@link #startShowingInputView()} and // {@link #onEvaluateInputViewShown()}. private boolean mIsExecutingStartShowingInputView; @@ -176,6 +158,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 +172,21 @@ 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; + private static final int MSG_RESUME_SUGGESTIONS_FOR_START_INPUT = 10; // Update this when adding new messages - private static final int MSG_LAST = MSG_WAIT_FOR_DICTIONARY_LOAD; + private static final int MSG_LAST = MSG_RESUME_SUGGESTIONS_FOR_START_INPUT; 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); } @@ -245,20 +230,26 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen break; case MSG_RESUME_SUGGESTIONS: latinIme.mInputLogic.restartSuggestionsOnWordTouchedByCursor( - latinIme.mSettings.getCurrent(), - msg.arg1 == ARG1_TRUE /* shouldIncludeResumedWordInSuggestions */, + latinIme.mSettings.getCurrent(), false /* forStartInput */, + latinIme.mKeyboardSwitcher.getCurrentKeyboardScriptId()); + break; + case MSG_RESUME_SUGGESTIONS_FOR_START_INPUT: + latinIme.mInputLogic.restartSuggestionsOnWordTouchedByCursor( + latinIme.mSettings.getCurrent(), true /* forStartInput */, 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 +266,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 +281,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES)); } - public void postResumeSuggestions(final boolean shouldIncludeResumedWordInSuggestions, - final boolean shouldDelay) { + private void postResumeSuggestionsInternal(final boolean shouldDelay, + final boolean forStartInput) { final LatinIME latinIme = getOwnerInstance(); if (latinIme == null) { return; @@ -297,17 +291,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } removeMessages(MSG_RESUME_SUGGESTIONS); + removeMessages(MSG_RESUME_SUGGESTIONS_FOR_START_INPUT); + final int message = forStartInput ? MSG_RESUME_SUGGESTIONS_FOR_START_INPUT + : MSG_RESUME_SUGGESTIONS; if (shouldDelay) { - sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS, - shouldIncludeResumedWordInSuggestions ? ARG1_TRUE : ARG1_FALSE, - 0 /* ignored */), mDelayInMillisecondsToUpdateSuggestions); + sendMessageDelayed(obtainMessage(message), + mDelayInMillisecondsToUpdateSuggestions); } else { - sendMessage(obtainMessage(MSG_RESUME_SUGGESTIONS, - shouldIncludeResumedWordInSuggestions ? ARG1_TRUE : ARG1_FALSE, - 0 /* ignored */)); + sendMessage(obtainMessage(message)); } } + public void postResumeSuggestions(final boolean shouldDelay) { + postResumeSuggestionsInternal(shouldDelay, false /* forStartInput */); + } + + public void postResumeSuggestionsForStartInput(final boolean shouldDelay) { + postResumeSuggestionsInternal(shouldDelay, true /* forStartInput */); + } + public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) { removeMessages(MSG_RESET_CACHES); sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0, @@ -316,7 +318,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 +347,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 +457,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 +465,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen latinIme.onStartInputViewInternal(editorInfo, restarting); mAppliedEditorInfo = editorInfo; } + cancelDeallocateMemory(); } } @@ -463,6 +479,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen latinIme.onFinishInputViewInternal(finishingInput); mAppliedEditorInfo = null; } + if (!hasPendingDeallocateMemory()) { + postDeallocateMemory(); + } } } @@ -516,9 +535,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public LatinIME() { super(); 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); @@ -530,28 +548,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen DebugFlags.init(PreferenceManager.getDefaultSharedPreferences(this)); RichInputMethodManager.init(this); mRichImm = RichInputMethodManager.getInstance(); - SubtypeSwitcher.init(this); KeyboardSwitcher.init(this); AudioAndHapticFeedbackManager.init(this); AccessibilityUtils.init(this); - StatsUtils.init(this); - + mStatsUtilsManager.onCreate(this /* context */, mDictionaryFacilitator); 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(); + resetDictionaryFacilitatorIfNecessary(); - // Register to receive ringer mode change and network state change. - // Also receive installation and removal of a dictionary pack. + // Register to receive ringer mode change. final IntentFilter filter = new IntentFilter(); - filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); - registerReceiver(mConnectivityAndRingerModeChangeReceiver, filter); + registerReceiver(mRingerModeChangeReceiver, filter); + // Register to receive installation and removal of a dictionary pack. final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); @@ -566,15 +581,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen dictDumpFilter.addAction(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION); 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 locale = mRichImm.getCurrentSubtypeLocale(); final EditorInfo editorInfo = getCurrentInputEditorInfo(); final InputAttributes inputAttributes = new InputAttributes( editorInfo, isFullscreenMode(), getPackageName()); @@ -585,30 +598,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // been displayed. Opening dictionaries never affects responsivity as dictionaries are // asynchronously loaded. if (!mHandler.hasPendingReopenDictionaries()) { - resetSuggestForLocale(locale); + resetDictionaryFacilitator(locale); } - mDictionaryFacilitator.updateEnabledSubtypes(mRichImm.getMyEnabledInputMethodSubtypeList( - true /* allowsImplicitlySelectedSubtypes */)); refreshPersonalizationDictionarySession(currentSettingsValues); - StatsUtils.onLoadSettings(currentSettingsValues); + resetDictionaryFacilitatorIfNecessary(); + mStatsUtilsManager.onLoadSettings(this /* context */, currentSettingsValues); } private void refreshPersonalizationDictionarySession( final SettingsValues currentSettingsValues) { - mPersonalizationDictionaryUpdater.onLoadSettings( - currentSettingsValues.mUsePersonalizedDicts, - mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes()); - mContextualDictionaryUpdater.onLoadSettings(currentSettingsValues.mUsePersonalizedDicts); - final boolean shouldKeepUserHistoryDictionaries; - if (currentSettingsValues.mUsePersonalizedDicts) { - shouldKeepUserHistoryDictionaries = true; - } else { - shouldKeepUserHistoryDictionaries = false; - } - if (!shouldKeepUserHistoryDictionaries) { + if (!currentSettingsValues.mUsePersonalizedDicts) { // Remove user history dictionaries. PersonalizationHelper.removeAllUserHistoryDictionaries(this); - mDictionaryFacilitator.clearUserHistoryDictionary(); + mDictionaryFacilitator.clearUserHistoryDictionary(this); } } @@ -621,44 +623,49 @@ 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(); + void resetDictionaryFacilitatorIfNecessary() { + final Locale subtypeSwitcherLocale = mRichImm.getCurrentSubtypeLocale(); final Locale subtypeLocale; - if (TextUtils.isEmpty(switcherLocaleStr)) { + if (subtypeSwitcherLocale == null) { // 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; } else { - subtypeLocale = switcherSubtypeLocale; + subtypeLocale = subtypeSwitcherLocale; + } + if (mDictionaryFacilitator.isForLocale(subtypeLocale) + && mDictionaryFacilitator.isForAccount(mSettings.getCurrent().mAccount)) { + return; } - resetSuggestForLocale(subtypeLocale); + resetDictionaryFacilitator(subtypeLocale); } /** - * Reset suggest by loading dictionaries for the locale and the current settings values. + * Reset the facilitator by loading dictionaries for the given locale and + * the current settings values. * * @param locale the locale */ - private void resetSuggestForLocale(final Locale locale) { + // TODO: make sure the current settings always have the right locales, and read from them. + private void resetDictionaryFacilitator(final Locale locale) { final SettingsValues settingsValues = mSettings.getCurrent(); mDictionaryFacilitator.resetDictionaries(this /* context */, locale, settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts, - false /* forceReloadMainDictionary */, this); + false /* forceReloadMainDictionary */, + settingsValues.mAccount, "" /* dictNamePrefix */, + this /* DictionaryInitializationListener */); if (settingsValues.mAutoCorrectionEnabledPerUserSettings) { mInputLogic.mSuggest.setAutoCorrectionThreshold( settingsValues.mAutoCorrectionThreshold); } + mInputLogic.mSuggest.setPlausibilityThreshold(settingsValues.mPlausibilityThreshold); } /** @@ -668,19 +675,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final SettingsValues settingsValues = mSettings.getCurrent(); mDictionaryFacilitator.resetDictionaries(this /* context */, mDictionaryFacilitator.getLocale(), settingsValues.mUseContactsDict, - settingsValues.mUsePersonalizedDicts, true /* forceReloadMainDictionary */, this); + settingsValues.mUsePersonalizedDicts, + true /* forceReloadMainDictionary */, + settingsValues.mAccount, "" /* dictNamePrefix */, + this /* DictionaryInitializationListener */); } @Override public void onDestroy() { mDictionaryFacilitator.closeDictionaries(); - mPersonalizationDictionaryUpdater.onDestroy(); - mContextualDictionaryUpdater.onDestroy(); mSettings.onDestroy(); - unregisterReceiver(mConnectivityAndRingerModeChangeReceiver); + unregisterReceiver(mRingerModeChangeReceiver); unregisterReceiver(mDictionaryPackInstallReceiver); unregisterReceiver(mDictionaryDumpBroadcastReceiver); - StatsUtils.onDestroy(); + mStatsUtilsManager.onDestroy(this /* context */); super.onDestroy(); } @@ -688,7 +696,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void recycle() { unregisterReceiver(mDictionaryPackInstallReceiver); unregisterReceiver(mDictionaryDumpBroadcastReceiver); - unregisterReceiver(mConnectivityAndRingerModeChangeReceiver); + unregisterReceiver(mRingerModeChangeReceiver); mInputLogic.recycle(); } @@ -715,15 +723,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,60 +736,17 @@ 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); } - mInputLogic.setTextDecoratorUi(new TextDecoratorUi(this, view)); - } - - @Override - public void setExtractView(final View view) { - final TextView prevExtractEditText = mExtractEditText; - super.setExtractView(view); - TextView nextExtractEditText = null; - if (view != null) { - final View extractEditText = view.findViewById(android.R.id.inputExtractEditText); - if (extractEditText instanceof TextView) { - nextExtractEditText = (TextView)extractEditText; - } - } - if (prevExtractEditText == nextExtractEditText) { - return; - } - if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && prevExtractEditText != null) { - prevExtractEditText.getViewTreeObserver().removeOnPreDrawListener( - mExtractTextViewPreDrawListener); - } - mExtractEditText = nextExtractEditText; - if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && mExtractEditText != null) { - mExtractEditText.getViewTreeObserver().addOnPreDrawListener( - mExtractTextViewPreDrawListener); - } - } - - private final ViewTreeObserver.OnPreDrawListener mExtractTextViewPreDrawListener = - new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - onExtractTextViewPreDraw(); - 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. - return; } @Override @@ -795,11 +757,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 @@ -811,20 +777,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) { // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() // is not guaranteed. It may even be called at the same time on a different thread. - mSubtypeSwitcher.onSubtypeChanged(subtype); + InputMethodSubtype oldSubtype = mRichImm.getCurrentSubtype().getRawSubtype(); + StatsUtils.onSubtypeChanged(oldSubtype, subtype); + mRichImm.onSubtypeChanged(subtype); mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype), mSettings.getCurrent()); 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); - mRichImm.clearSubtypeCaches(); + + mDictionaryFacilitator.onStartInput(); + // 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.refreshSubtypeCaches(); final KeyboardSwitcher switcher = mKeyboardSwitcher; switcher.updateKeyboardTheme(); final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView(); @@ -839,7 +812,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } return; } - if (DEBUG) { + if (DebugFlags.DEBUG_ENABLED) { Log.d(TAG, "onStartInputView: editorInfo:" + String.format("inputType=0x%08x imeOptions=0x%08x", editorInfo.inputType, editorInfo.imeOptions)); @@ -867,6 +840,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(), + mRichImm.getCurrentSubtypeLocale(), + switcher.getKeyboard()); + // Forward this event to the accessibility utilities, if enabled. final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); if (accessUtils.isTouchExplorationEnabled()) { @@ -875,9 +854,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo); final boolean isDifferentTextField = !restarting || inputTypeChanged; - if (isDifferentTextField) { - mSubtypeSwitcher.updateParametersOnStartInputView(); - } + + StatsUtils.onStartInputView(editorInfo.inputType, + Settings.getInstance().getCurrent().mDisplayOrientation, + !isDifferentTextField); // The EditorInfo might have a flag that affects fullscreen mode. // Note: This call should be done by InputMethodService? @@ -897,15 +877,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // span, so we should reset our state unconditionally, even if restarting is true. // We also tell the input logic about the combining rules for the current subtype, so // it can adjust its combiners if needed. - mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype(), + mInputLogic.startInput(mRichImm.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 +894,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.postResumeSuggestionsForStartInput(true /* shouldDelay */); needToCallLoadKeyboardLater = false; } } else { @@ -943,6 +919,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen suggest.setAutoCorrectionThreshold( currentSettingsValues.mAutoCorrectionThreshold); } + suggest.setPlausibilityThreshold(currentSettingsValues.mPlausibilityThreshold); switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); @@ -970,7 +947,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mHandler.cancelUpdateSuggestionStrip(); mainKeyboardView.setMainDictionaryAvailability( - mDictionaryFacilitator.hasInitializedMainDictionary()); + mDictionaryFacilitator.hasAtLeastOneInitializedMainDictionary()); mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn, currentSettingsValues.mKeyPreviewPopupDismissDelay); mainKeyboardView.setSlidingKeyInputPreviewEnabled( @@ -980,8 +957,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen currentSettingsValues.mGestureTrailEnabled, currentSettingsValues.mGestureFloatingPreviewTextEnabled); - // Contextual dictionary should be updated for the current application. - mContextualDictionaryUpdater.onStartInputView(editorInfo.packageName); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } @@ -994,45 +969,50 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - private void onFinishInputInternal() { + void onFinishInputInternal() { super.onFinishInput(); + mDictionaryFacilitator.onFinishInput(); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } } - 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, final int composingSpanStart, final int composingSpanEnd) { super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, composingSpanEnd); - if (DEBUG) { + if (DebugFlags.DEBUG_ENABLED) { Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart + ", ose=" + oldSelEnd + ", nss=" + newSelStart + ", nse=" + newSelEnd + ", cs=" + composingSpanStart + ", ce=" + composingSpanEnd); } - // This call happens when we have a hardware keyboard as well as when we don't. While we - // don't support hardware keyboards yet we should avoid doing the processing associated - // with cursor movement when we have a hardware keyboard since we are not in charge. + // This call happens whether our view is displayed or not, but if it's not then we should + // not attempt recorrection. This is true even with a hardware keyboard connected: if the + // view is not displayed we have no means of showing suggestions anyway, and if it is then + // we want to show suggestions anyway. final SettingsValues settingsValues = mSettings.getCurrent(); - if ((!settingsValues.mHasHardwareKeyboard || ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) + if (isInputViewShown() && mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, settingsValues)) { mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), @@ -1040,15 +1020,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - // We cannot mark this method as @Override until new SDK becomes publicly available. - // @Override - public void onUpdateCursorAnchorInfo(final CursorAnchorInfo info) { - if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || isFullscreenMode()) { - return; - } - mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); - } - /** * This is called when the user has clicked on the extracted text view, * when running in fullscreen mode. The default implementation hides @@ -1098,7 +1069,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) { - if (DEBUG) { + if (DebugFlags.DEBUG_ENABLED) { Log.i(TAG, "Received completions:"); if (applicationSpecifiedCompletions != null) { for (int i = 0; i < applicationSpecifiedCompletions.length; i++) { @@ -1121,9 +1092,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen SuggestedWords.getFromApplicationSpecifiedCompletions( applicationSpecifiedCompletions); final SuggestedWords suggestedWords = new SuggestedWords(applicationSuggestedWords, - null /* rawSuggestions */, false /* typedWordValid */, false /* willAutoCorrect */, + null /* rawSuggestions */, + null /* typedWord */, + false /* typedWordValid */, + false /* willAutoCorrect */, false /* isObsoleteSuggestions */, - SuggestedWords.INPUT_STYLE_APPLICATION_SPECIFIED /* inputStyle */); + SuggestedWords.INPUT_STYLE_APPLICATION_SPECIFIED /* inputStyle */, + SuggestedWords.NOT_A_SEQUENCE_NUMBER); // When in fullscreen mode, show completions generated by the application forcibly setSuggestedWords(suggestedWords); } @@ -1131,6 +1106,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()) { @@ -1141,8 +1120,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (hasHardwareKeyboard && visibleKeyboardView.getVisibility() == View.GONE) { // If there is a hardware keyboard and a visible software keyboard view has been hidden, // no visual element will be shown on the screen. - outInsets.touchableInsets = inputHeight; + outInsets.contentTopInsets = inputHeight; outInsets.visibleTopInsets = inputHeight; + mInsetsUpdater.setInsets(outInsets); return; } final int suggestionsHeight = (!mKeyboardSwitcher.isShowingEmojiPalettes() @@ -1150,7 +1130,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen ? mSuggestionStripView.getHeight() : 0; final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - suggestionsHeight; mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY); - // Need to set touchable region only if a keyboard view is being shown. + // Need to set expanded touchable region only if a keyboard view is being shown. if (visibleKeyboardView.isShown()) { final int touchLeft = 0; final int touchTop = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY; @@ -1163,14 +1143,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 +1162,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 +1199,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void updateFullscreenMode() { + super.updateFullscreenMode(); + 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 +1224,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. @@ -1256,24 +1247,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return keyboard.getCoordinates(codePoints); } - // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is - // pressed. - @Override - public void addWordToUserDictionary(final String word) { - if (TextUtils.isEmpty(word)) { - // 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); - mInputLogic.onAddWordToUserDictionary(); - } - // Callback for the {@link SuggestionStripView}, to call when the important notice strip is // pressed. @Override @@ -1284,7 +1257,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener} @Override public void onClickSettingsOfImportantNoticeDialog(final int nextVersion) { - launchSettings(); + launchSettings(SettingsActivity.EXTRA_ENTRY_VALUE_NOTICE_DIALOG); } // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener} @@ -1328,48 +1301,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) { - mSubtypeSwitcher.switchToShortcutIME(this); - // Still call the *#onCodeInput methods for readability. + 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) { + mRichImm.switchToShortcutIme(this); } - 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,44 +1368,59 @@ 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( + mRichImm.getCurrentSubtypeLocale(), + mKeyboardSwitcher.getKeyboard()); } @Override public void onUpdateBatchInput(final InputPointers batchPointers) { - mInputLogic.onUpdateBatchInput(mSettings.getCurrent(), batchPointers, mKeyboardSwitcher); + mInputLogic.onUpdateBatchInput(batchPointers); } @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 suggested words by the + * IME for the full gesture, possibly updating the TextView to reflect the first suggestion. + * <p> + * This method must be run on the UI Thread. + * @param suggestedWords suggested words by the IME for the full gesture. + */ + public void onTailBatchInputResultShown(final SuggestedWords suggestedWords) { + mGestureConsumer.onImeSuggestionsProcessed(suggestedWords, + mInputLogic.getComposingStart(), mInputLogic.getComposingLength(), + mDictionaryFacilitator); } // 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 @@ -1446,22 +1442,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return null != mSuggestionStripView; } - @Override - public boolean isShowingAddToDictionaryHint() { - return hasSuggestionStripView() && mSuggestionStripView.isShowingAddToDictionaryHint(); - } - - @Override - public void dismissAddToDictionaryHint() { - if (!hasSuggestionStripView()) { - return; - } - mSuggestionStripView.dismissAddToDictionaryHint(); - } - 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; @@ -1471,7 +1454,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } final boolean shouldShowImportantNotice = - ImportantNoticeUtils.shouldShowImportantNotice(this); + ImportantNoticeUtils.shouldShowImportantNotice(this, currentSettingsValues); final boolean shouldShowSuggestionCandidates = currentSettingsValues.mInputAttributes.mShouldShowSuggestions && currentSettingsValues.isSuggestionsEnabledPerUserSettings(); @@ -1489,7 +1472,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 +1490,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())); + mRichImm.getCurrentSubtype().isRtlSubtype()); } } @@ -1516,26 +1499,23 @@ 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(), + mInputLogic.getSuggestedWords(mSettings.getCurrent(), keyboard, mKeyboardSwitcher.getKeyboardShiftMode(), inputStyle, sequenceNumber, callback); } @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); } // 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); + AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords); } // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} @@ -1550,25 +1530,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen updateStateAfterInputTransaction(completeInputTransaction); } - @Override - public void showAddToDictionaryHint(final String word) { - if (!hasSuggestionStripView()) { - return; - } - mSuggestionStripView.showAddToDictionaryHint(word); - } - // This will show either an empty suggestion strip (if prediction is enabled) or // punctuation suggestions (if it's disabled). @Override 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); } - // TODO: Make this private // Outside LatinIME, only used by the {@link InputTestsBase} test suite. @UsedForTesting void loadKeyboard() { @@ -1676,7 +1648,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 +1670,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); } @@ -1713,21 +1687,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // boolean onKeyLongPress(final int keyCode, final KeyEvent event); // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event); - // receive ringer mode change and network state change. - private final BroadcastReceiver mConnectivityAndRingerModeChangeReceiver = - new BroadcastReceiver() { + // receive ringer mode change. + private final BroadcastReceiver mRingerModeChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); - if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { - mSubtypeSwitcher.onNetworkStateChanged(intent); - } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { + if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged(); } } }; - private void launchSettings() { + void launchSettings(final String extraEntryValue) { mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR); requestHideSelf(0); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); @@ -1740,6 +1711,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.putExtra(SettingsActivity.EXTRA_SHOW_HOME_AS_UP, false); + intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY, extraEntryValue); startActivity(intent); } @@ -1751,6 +1723,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 +1731,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); @@ -1766,7 +1739,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen startActivity(intent); break; case 1: - launchSettings(); + launchSettings(SettingsActivity.EXTRA_ENTRY_VALUE_LONG_PRESS_COMMA); break; } } @@ -1798,45 +1771,45 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen dialog.show(); } - // TODO: can this be removed somehow without breaking the tests? @UsedForTesting - /* package for test */ SuggestedWords getSuggestedWordsForTest() { + SuggestedWords getSuggestedWordsForTest() { // You may not use this method for anything else than debug - return DEBUG ? mInputLogic.mSuggestedWords : null; + return DebugFlags.DEBUG_ENABLED ? mInputLogic.mSuggestedWords : null; } // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME. @UsedForTesting - /* package for test */ void waitForLoadingDictionaries(final long timeout, final TimeUnit unit) + void waitForLoadingDictionaries(final long timeout, final TimeUnit unit) throws InterruptedException { mDictionaryFacilitator.waitForLoadingDictionariesForTesting(timeout, unit); } // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly. @UsedForTesting - /* package for test */ void replaceDictionariesForTest(final Locale locale) { + void replaceDictionariesForTest(final Locale locale) { final SettingsValues settingsValues = mSettings.getCurrent(); mDictionaryFacilitator.resetDictionaries(this, locale, settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts, - false /* forceReloadMainDictionary */, this /* listener */); + false /* forceReloadMainDictionary */, + settingsValues.mAccount, "", /* dictionaryNamePrefix */ + this /* DictionaryInitializationListener */); } // DO NOT USE THIS for any other purpose than testing. @UsedForTesting - /* package for test */ void clearPersonalizedDictionariesForTest() { - mDictionaryFacilitator.clearUserHistoryDictionary(); - mDictionaryFacilitator.clearPersonalizationDictionary(); + void clearPersonalizedDictionariesForTest() { + mDictionaryFacilitator.clearUserHistoryDictionary(this); } @UsedForTesting - /* package for test */ List<InputMethodSubtype> getEnabledSubtypesForTest() { + List<InputMethodSubtype> getEnabledSubtypesForTest() { return (mRichImm != null) ? mRichImm.getMyEnabledInputMethodSubtypeList( true /* allowsImplicitlySelectedSubtypes */) : new ArrayList<InputMethodSubtype>(); } public void dumpDictionaryForDebug(final String dictName) { - if (mDictionaryFacilitator.getLocale() == null) { - resetSuggest(); + if (!mDictionaryFacilitator.isActive()) { + resetDictionaryFacilitatorIfNecessary(); } mDictionaryFacilitator.dumpDictionaryForDebug(dictName); } @@ -1862,6 +1835,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen p.println(" Keyboard mode = " + keyboardMode); final SettingsValues settingsValues = mSettings.getCurrent(); p.println(settingsValues.dump()); + p.println(mDictionaryFacilitator.dump(this /* context */)); // TODO: Dump all settings values } diff --git a/java/src/com/android/inputmethod/latin/NgramContext.java b/java/src/com/android/inputmethod/latin/NgramContext.java new file mode 100644 index 000000000..9682fb8a4 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/NgramContext.java @@ -0,0 +1,291 @@ +/* + * 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.text.TextUtils; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.common.StringUtils; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; + +import java.util.ArrayList; +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 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); + + public static final String BEGINNING_OF_SENTENCE_TAG = "<S>"; + + public static final String CONTEXT_SEPARATOR = " "; + + public static NgramContext getEmptyPrevWordsContext(int maxPrevWordCount) { + return new NgramContext(maxPrevWordCount, WordInfo.EMPTY_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); + @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; + // TODO: Have sentence separator. + // Whether the current context is beginning of sentence or not. This is true when composing + // at the beginning of an input field or composing a word after a sentence separator. + public final boolean mIsBeginningOfSentence; + + // Beginning of sentence. + private WordInfo() { + mWord = ""; + mIsBeginningOfSentence = true; + } + + public WordInfo(final CharSequence word) { + mWord = word; + mIsBeginningOfSentence = false; + } + + public boolean isValid() { + return mWord != null; + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] { mWord, mIsBeginningOfSentence } ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof WordInfo)) return false; + final WordInfo wordInfo = (WordInfo)o; + if (mWord == null || wordInfo.mWord == null) { + return mWord == wordInfo.mWord + && mIsBeginningOfSentence == wordInfo.mIsBeginningOfSentence; + } + return TextUtils.equals(mWord, wordInfo.mWord) + && mIsBeginningOfSentence == wordInfo.mIsBeginningOfSentence; + } + } + + // The words immediately before the considered word. EMPTY_WORD_INFO element means we don't + // have any context for that previous word including the "beginning of sentence context" - we + // just don't know what to predict using the information. An example of that is after a comma. + // 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. + private final WordInfo[] mPrevWordsInfo; + private final int mPrevWordsCount; + + private final int mMaxPrevWordCount; + + // Construct from the previous word information. + public NgramContext(final WordInfo... prevWordsInfo) { + this(DecoderSpecificConstants.MAX_PREV_WORD_COUNT_FOR_N_GRAM, prevWordsInfo); + } + + public NgramContext(final int maxPrevWordCount, final WordInfo... prevWordsInfo) { + mPrevWordsInfo = prevWordsInfo; + mPrevWordsCount = prevWordsInfo.length; + mMaxPrevWordCount = maxPrevWordCount; + } + + /** + * Create next prevWordsInfo using current prevWordsInfo. + */ + @Nonnull + public NgramContext getNextNgramContext(final WordInfo wordInfo) { + final int nextPrevWordCount = Math.min(mMaxPrevWordCount, mPrevWordsCount + 1); + final WordInfo[] prevWordsInfo = new WordInfo[nextPrevWordCount]; + prevWordsInfo[0] = wordInfo; + System.arraycopy(mPrevWordsInfo, 0, prevWordsInfo, 1, nextPrevWordCount - 1); + return new NgramContext(mMaxPrevWordCount, prevWordsInfo); + } + + + /** + * Extracts the previous words context. + * + * @return a String with the previous words separated by white space. + */ + public String extractPrevWordsContext() { + final ArrayList<String> terms = new ArrayList<>(); + for (int i = mPrevWordsInfo.length - 1; i >= 0; --i) { + if (mPrevWordsInfo[i] != null && mPrevWordsInfo[i].isValid()) { + final NgramContext.WordInfo wordInfo = mPrevWordsInfo[i]; + if (wordInfo.mIsBeginningOfSentence) { + terms.add(BEGINNING_OF_SENTENCE_TAG); + } else { + final String term = wordInfo.mWord.toString(); + if (!term.isEmpty()) { + terms.add(term); + } + } + } + } + return TextUtils.join(CONTEXT_SEPARATOR, terms); + } + + /** + * Extracts the previous words context. + * + * @return a String array with the previous words. + */ + public String[] extractPrevWordsContextArray() { + final ArrayList<String> prevTermList = new ArrayList<>(); + for (int i = mPrevWordsInfo.length - 1; i >= 0; --i) { + if (mPrevWordsInfo[i] != null && mPrevWordsInfo[i].isValid()) { + final NgramContext.WordInfo wordInfo = mPrevWordsInfo[i]; + if (wordInfo.mIsBeginningOfSentence) { + prevTermList.add(BEGINNING_OF_SENTENCE_TAG); + } else { + final String term = wordInfo.mWord.toString(); + if (!term.isEmpty()) { + prevTermList.add(term); + } + } + } + } + final String[] contextStringArray = prevTermList.toArray(new String[prevTermList.size()]); + return contextStringArray; + } + + public boolean 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 isNthPrevWordBeginningOfSentence(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 < mPrevWordsCount; i++) { + final WordInfo wordInfo = mPrevWordsInfo[i]; + if (wordInfo == null || !wordInfo.isValid()) { + codePointArrays[i] = new int[0]; + isBeginningOfSentenceArray[i] = false; + continue; + } + codePointArrays[i] = StringUtils.toCodePointArray(wordInfo.mWord); + isBeginningOfSentenceArray[i] = wordInfo.mIsBeginningOfSentence; + } + } + + public int getPrevWordCount() { + return mPrevWordsCount; + } + + @Override + public int hashCode() { + 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 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 < mPrevWordsCount; i++) { + final WordInfo wordInfo = mPrevWordsInfo[i]; + builder.append("PrevWord["); + builder.append(i); + builder.append("]: "); + if (wordInfo == null) { + builder.append("null. "); + continue; + } + if (!wordInfo.isValid()) { + builder.append("Empty. "); + continue; + } + builder.append(wordInfo.mWord); + builder.append(", isBeginningOfSentence: "); + builder.append(wordInfo.mIsBeginningOfSentence); + builder.append(". "); + } + return builder.toString(); + } +} diff --git a/java/src/com/android/inputmethod/latin/PrevWordsInfo.java b/java/src/com/android/inputmethod/latin/PrevWordsInfo.java deleted file mode 100644 index db877ab7a..000000000 --- a/java/src/com/android/inputmethod/latin/PrevWordsInfo.java +++ /dev/null @@ -1,164 +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; - -import android.text.TextUtils; - -import com.android.inputmethod.latin.utils.StringUtils; - -import java.util.Arrays; - -/** - * 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); - - /** - * Word information used to represent previous words information. - */ - public static class WordInfo { - public static final WordInfo EMPTY_WORD_INFO = new WordInfo(null); - public static final WordInfo BEGINNING_OF_SENTENCE = new WordInfo(); - - // This is an empty char sequence when mIsBeginningOfSentence is true. - public final CharSequence mWord; - // TODO: Have sentence separator. - // Whether the current context is beginning of sentence or not. This is true when composing - // at the beginning of an input field or composing a word after a sentence separator. - public final boolean mIsBeginningOfSentence; - - // Beginning of sentence. - public WordInfo() { - mWord = ""; - mIsBeginningOfSentence = true; - } - - public WordInfo(final CharSequence word) { - mWord = word; - mIsBeginningOfSentence = false; - } - - public boolean isValid() { - return mWord != null; - } - - @Override - public int hashCode() { - return Arrays.hashCode(new Object[] { mWord, mIsBeginningOfSentence } ); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof WordInfo)) return false; - final WordInfo wordInfo = (WordInfo)o; - if (mWord == null || wordInfo.mWord == null) { - return mWord == wordInfo.mWord - && mIsBeginningOfSentence == wordInfo.mIsBeginningOfSentence; - } - return TextUtils.equals(mWord, wordInfo.mWord) - && mIsBeginningOfSentence == wordInfo.mIsBeginningOfSentence; - } - } - - // The words immediately before the considered word. EMPTY_WORD_INFO element means we don't - // have any context for that previous word including the "beginning of sentence context" - we - // just don't know what to predict using the information. An example of that is after a comma. - // 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]; - - // 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; - } - } - - // 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]; - prevWordsInfo[0] = wordInfo; - for (int i = 1; i < prevWordsInfo.length; i++) { - prevWordsInfo[i] = mPrevWordsInfo[i - 1]; - } - return new PrevWordsInfo(prevWordsInfo); - } - - public boolean isValid() { - return mPrevWordsInfo[0].isValid(); - } - - public void outputToArray(final int[][] codePointArrays, - final boolean[] isBeginningOfSentenceArray) { - for (int i = 0; i < mPrevWordsInfo.length; i++) { - final WordInfo wordInfo = mPrevWordsInfo[i]; - if (wordInfo == null || !wordInfo.isValid()) { - codePointArrays[i] = new int[0]; - isBeginningOfSentenceArray[i] = false; - continue; - } - codePointArrays[i] = StringUtils.toCodePointArray(wordInfo.mWord); - isBeginningOfSentenceArray[i] = wordInfo.mIsBeginningOfSentence; - } - } - - @Override - public int hashCode() { - return Arrays.hashCode(mPrevWordsInfo); - } - - @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); - } - - @Override - public String toString() { - final StringBuffer builder = new StringBuffer(); - for (int i = 0; i < mPrevWordsInfo.length; i++) { - final WordInfo wordInfo = mPrevWordsInfo[i]; - builder.append("PrevWord["); - builder.append(i); - builder.append("]: "); - if (wordInfo == null || !wordInfo.isValid()) { - builder.append("Empty. "); - continue; - } - builder.append(wordInfo.mWord); - builder.append(", isBeginningOfSentence: "); - builder.append(wordInfo.mIsBeginningOfSentence); - builder.append(". "); - } - return builder.toString(); - } -} diff --git a/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java index 56014cbad..e2c562174 100644 --- a/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java +++ b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java @@ -17,11 +17,14 @@ 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; +import javax.annotation.Nullable; + /** * The extended {@link SuggestedWords} class to represent punctuation suggestions. * @@ -32,10 +35,12 @@ public final class PunctuationSuggestions extends SuggestedWords { private PunctuationSuggestions(final ArrayList<SuggestedWordInfo> punctuationsList) { super(punctuationsList, null /* rawSuggestions */, + null /* typedWord */, false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, false /* isObsoleteSuggestions */, - INPUT_STYLE_NONE /* inputStyle */); + INPUT_STYLE_NONE /* inputStyle */, + SuggestedWords.NOT_A_SEQUENCE_NUMBER); } /** @@ -46,17 +51,21 @@ public final class PunctuationSuggestions extends SuggestedWords { * @return The {@link PunctuationSuggestions} object. */ public static PunctuationSuggestions newPunctuationSuggestions( - final String[] punctuationSpecs) { - final ArrayList<SuggestedWordInfo> puncuationsList = new ArrayList<>(); - for (final String puncSpec : punctuationSpecs) { - puncuationsList.add(newHardCodedWordInfo(puncSpec)); + @Nullable final String[] punctuationSpecs) { + if (punctuationSpecs == null || punctuationSpecs.length == 0) { + return new PunctuationSuggestions(new ArrayList<SuggestedWordInfo>(0)); + } + final ArrayList<SuggestedWordInfo> punctuationList = + new ArrayList<>(punctuationSpecs.length); + for (String spec : punctuationSpecs) { + punctuationList.add(newHardCodedWordInfo(spec)); } - return new PunctuationSuggestions(puncuationsList); + return new PunctuationSuggestions(punctuationList); } /** * {@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 +79,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 +91,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) { @@ -105,7 +114,8 @@ public final class PunctuationSuggestions extends SuggestedWords { } private static SuggestedWordInfo newHardCodedWordInfo(final String keySpec) { - return new SuggestedWordInfo(keySpec, SuggestedWordInfo.MAX_SCORE, + return new SuggestedWordInfo(keySpec, "" /* prevWordsContext */, + SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED, Dictionary.DICTIONARY_HARDCODED, SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 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..08e8fe346 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -16,13 +16,14 @@ package com.android.inputmethod.latin; -import android.graphics.Color; +import static com.android.inputmethod.latin.define.DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH; + import android.inputmethodservice.InputMethodService; import android.os.Build; +import android.os.Bundle; import android.text.SpannableStringBuilder; -import android.text.Spanned; import android.text.TextUtils; -import android.text.style.BackgroundColorSpan; +import android.text.style.CharacterStyle; import android.util.Log; import android.view.KeyEvent; import android.view.inputmethod.CompletionInfo; @@ -33,16 +34,20 @@ 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.UnicodeSurrogate; +import com.android.inputmethod.latin.common.StringUtils; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; +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,15 +57,15 @@ 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; private static final boolean DEBUG_BATCH_NESTING = false; // Provision for long words and separators between the words. - private static final int LOOKBACK_CHARACTER_NUM = Constants.DICTIONARY_MAX_WORD_LENGTH - * (Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1) /* words */ - + Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM /* separators */; + private static final int LOOKBACK_CHARACTER_NUM = DICTIONARY_MAX_WORD_LENGTH + * (DecoderSpecificConstants.MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1) /* words */ + + DecoderSpecificConstants.MAX_PREV_WORD_COUNT_FOR_N_GRAM /* separators */; private static final int INVALID_CURSOR_POSITION = -1; /** @@ -88,26 +93,25 @@ public final class RichInputConnection { private final StringBuilder mComposingText = new StringBuilder(); /** - * This variable is a temporary object used in - * {@link #commitTextWithBackgroundColor(CharSequence, int, int)} to avoid object creation. + * This variable is a temporary object used in {@link #commitText(CharSequence,int)} + * to avoid object creation. */ private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder(); - /** - * This variable is used to track whether the last committed text had the background color or - * not. - * TODO: Omit this flag if possible. - */ - private boolean mLastCommittedTextHasBackgroundColor = false; private final InputMethodService mParent; InputConnection mIC; int mNestLevel; + public RichInputConnection(final InputMethodService parent) { mParent = parent; mIC = null; mNestLevel = 0; } + public boolean isConnected() { + return mIC != null; + } + private void checkConsistencyForDebug() { final ExtractedTextRequest r = new ExtractedTextRequest(); r.hintMaxChars = 0; @@ -143,15 +147,14 @@ public final class RichInputConnection { public void beginBatchEdit() { if (++mNestLevel == 1) { mIC = mParent.getCurrentInputConnection(); - if (null != mIC) { + if (isConnected()) { mIC.beginBatchEdit(); } } 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(); @@ -159,7 +162,7 @@ public final class RichInputConnection { public void endBatchEdit() { if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead - if (--mNestLevel == 0 && null != mIC) { + if (--mNestLevel == 0 && isConnected()) { mIC.endBatchEdit(); } if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); @@ -191,7 +194,7 @@ public final class RichInputConnection { Log.d(TAG, "Will try to retrieve text later."); return false; } - if (null != mIC && shouldFinishComposition) { + if (isConnected() && shouldFinishComposition) { mIC.finishComposingText(); } return true; @@ -207,8 +210,9 @@ public final class RichInputConnection { mIC = mParent.getCurrentInputConnection(); // Call upon the inputconnection directly since our own method is using the cache, and // we want to refresh it. - final CharSequence textBeforeCursor = null == mIC ? null : - mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); + final CharSequence textBeforeCursor = isConnected() + ? mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0) + : null; if (null == textBeforeCursor) { // For some reason the app thinks we are not connected to it. This looks like a // framework bug... Fall back to ground state and return false. @@ -237,39 +241,18 @@ public final class RichInputConnection { // it works, but it's wrong and should be fixed. mCommittedTextBeforeComposingText.append(mComposingText); mComposingText.setLength(0); - // TODO: Clear this flag in setComposingRegion() and setComposingText() as well if needed. - mLastCommittedTextHasBackgroundColor = false; - if (null != mIC) { + if (isConnected()) { mIC.finishComposingText(); } } /** - * Synonym of {@code commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT}. + * Calls {@link InputConnection#commitText(CharSequence, int)}. + * * @param text The text to commit. This may include styles. - * See {@link InputConnection#commitText(CharSequence, int)}. * @param newCursorPosition The new cursor position around the text. - * See {@link InputConnection#commitText(CharSequence, int)}. */ public void commitText(final CharSequence text, final int newCursorPosition) { - commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT, text.length()); - } - - /** - * Calls {@link InputConnection#commitText(CharSequence, int)} with the given background color. - * @param text The text to commit. This may include styles. - * See {@link InputConnection#commitText(CharSequence, int)}. - * @param newCursorPosition The new cursor position around the text. - * See {@link InputConnection#commitText(CharSequence, int)}. - * @param color The background color to be attached. Set {@link Color#TRANSPARENT} to disable - * the background color. Note that this method specifies {@link BackgroundColorSpan} with - * {@link Spanned#SPAN_COMPOSING} flag, meaning that the background color persists until - * {@link #finishComposingText()} is called. - * @param coloredTextLength the length of text, in Java chars, which should be rendered with - * the given background color. - */ - public void commitTextWithBackgroundColor(final CharSequence text, final int newCursorPosition, - final int color, final int coloredTextLength) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); mCommittedTextBeforeComposingText.append(text); @@ -279,46 +262,34 @@ public final class RichInputConnection { mExpectedSelStart += text.length() - mComposingText.length(); mExpectedSelEnd = mExpectedSelStart; mComposingText.setLength(0); - mLastCommittedTextHasBackgroundColor = false; - if (null != mIC) { - if (color == Color.TRANSPARENT) { - mIC.commitText(text, newCursorPosition); - } else { - mTempObjectForCommitText.clear(); - mTempObjectForCommitText.append(text); - final BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(color); - final int spanLength = Math.min(coloredTextLength, text.length()); - mTempObjectForCommitText.setSpan(backgroundColorSpan, 0, spanLength, - Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - mIC.commitText(mTempObjectForCommitText, newCursorPosition); - mLastCommittedTextHasBackgroundColor = true; + if (isConnected()) { + mTempObjectForCommitText.clear(); + mTempObjectForCommitText.append(text); + final CharacterStyle[] spans = mTempObjectForCommitText.getSpans( + 0, text.length(), CharacterStyle.class); + for (final CharacterStyle span : spans) { + final int spanStart = mTempObjectForCommitText.getSpanStart(span); + final int spanEnd = mTempObjectForCommitText.getSpanEnd(span); + final int spanFlags = mTempObjectForCommitText.getSpanFlags(span); + // We have to adjust the end of the span to include an additional character. + // This is to avoid splitting a unicode surrogate pair. + // See com.android.inputmethod.latin.common.Constants.UnicodeSurrogate + // See https://b.corp.google.com/issues/19255233 + if (0 < spanEnd && spanEnd < mTempObjectForCommitText.length()) { + final char spanEndChar = mTempObjectForCommitText.charAt(spanEnd - 1); + final char nextChar = mTempObjectForCommitText.charAt(spanEnd); + if (UnicodeSurrogate.isLowSurrogate(spanEndChar) + && UnicodeSurrogate.isHighSurrogate(nextChar)) { + mTempObjectForCommitText.setSpan(span, spanStart, spanEnd + 1, spanFlags); + } + } } + mIC.commitText(mTempObjectForCommitText, newCursorPosition); } } - /** - * Removes the background color from the highlighted text if necessary. Should be called while - * there is no on-going composing text. - * - * <p>CAVEAT: This method internally calls {@link InputConnection#finishComposingText()}. - * Be careful of any unexpected side effects.</p> - */ - public void removeBackgroundColorFromHighlightedTextIfNecessary() { - // TODO: We haven't yet full tested if we really need to check this flag or not. Omit this - // flag if everything works fine without this condition. - if (!mLastCommittedTextHasBackgroundColor) { - return; - } - if (mComposingText.length() > 0) { - Log.e(TAG, "clearSpansWithComposingFlags should be called when composing text is " + - "empty. mComposingText=" + mComposingText); - return; - } - finishComposingText(); - } - public CharSequence getSelectedText(final int flags) { - return (null == mIC) ? null : mIC.getSelectedText(flags); + return isConnected() ? mIC.getSelectedText(flags) : null; } public boolean canDeleteCharacters() { @@ -343,16 +314,17 @@ public final class RichInputConnection { public int getCursorCapsMode(final int inputType, final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) { mIC = mParent.getCurrentInputConnection(); - if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF; + if (!isConnected()) { + return Constants.TextUtils.CAP_MODE_OFF; + } if (!TextUtils.isEmpty(mComposingText)) { if (hasSpaceBefore) { // 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 +339,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); } @@ -402,12 +376,12 @@ public final class RichInputConnection { return s; } mIC = mParent.getCurrentInputConnection(); - return (null == mIC) ? null : mIC.getTextBeforeCursor(n, flags); + return isConnected() ? mIC.getTextBeforeCursor(n, flags) : null; } public CharSequence getTextAfterCursor(final int n, final int flags) { mIC = mParent.getCurrentInputConnection(); - return (null == mIC) ? null : mIC.getTextAfterCursor(n, flags); + return isConnected() ? mIC.getTextAfterCursor(n, flags) : null; } public void deleteSurroundingText(final int beforeLength, final int afterLength) { @@ -434,7 +408,7 @@ public final class RichInputConnection { mExpectedSelEnd -= mExpectedSelStart; mExpectedSelStart = 0; } - if (null != mIC) { + if (isConnected()) { mIC.deleteSurroundingText(beforeLength, afterLength); } if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); @@ -442,7 +416,7 @@ public final class RichInputConnection { public void performEditorAction(final int actionId) { mIC = mParent.getCurrentInputConnection(); - if (null != mIC) { + if (isConnected()) { mIC.performEditorAction(actionId); } } @@ -494,7 +468,7 @@ public final class RichInputConnection { break; } } - if (null != mIC) { + if (isConnected()) { mIC.sendKeyEvent(keyEvent); } } @@ -517,7 +491,7 @@ public final class RichInputConnection { mCommittedTextBeforeComposingText.append( textBeforeCursor.subSequence(0, indexOfStartOfComposingText)); } - if (null != mIC) { + if (isConnected()) { mIC.setComposingRegion(start, end); } } @@ -531,7 +505,7 @@ public final class RichInputConnection { mComposingText.append(text); // TODO: support values of newCursorPosition != 1. At this time, this is never called with // newCursorPosition != 1. - if (null != mIC) { + if (isConnected()) { mIC.setComposingText(text, newCursorPosition); } if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); @@ -556,7 +530,7 @@ public final class RichInputConnection { } mExpectedSelStart = start; mExpectedSelEnd = end; - if (null != mIC) { + if (isConnected()) { final boolean isIcValid = mIC.setSelection(start, end); if (!isIcValid) { return false; @@ -570,7 +544,7 @@ public final class RichInputConnection { if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); // This has no effect on the text field and does not change its content. It only makes // TextView flash the text for a second based on indices contained in the argument. - if (null != mIC) { + if (isConnected()) { mIC.commitCorrection(correctionInfo); } if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); @@ -586,18 +560,19 @@ public final class RichInputConnection { mExpectedSelStart += text.length() - mComposingText.length(); mExpectedSelEnd = mExpectedSelStart; mComposingText.setLength(0); - if (null != mIC) { + if (isConnected()) { mIC.commitCompletion(completionInfo); } if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } @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; + if (!isConnected()) { + return NgramContext.EMPTY_PREV_WORDS_INFO; } final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); if (DEBUG_PREVIOUS_TEXT && null != prev) { @@ -618,14 +593,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. @@ -645,7 +616,7 @@ public final class RichInputConnection { public TextRange getWordRangeAtCursor(final SpacingAndPunctuations spacingAndPunctuations, final int scriptId) { mIC = mParent.getCurrentInputConnection(); - if (mIC == null) { + if (!isConnected()) { return null; } final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, @@ -740,17 +711,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 +820,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 = isConnected() ? mIC.getSelectedText(0 /* flags */) : null; + 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 +868,15 @@ public final class RichInputConnection { } } + @Override + public boolean performPrivateCommand(final String action, final Bundle data) { + mIC = mParent.getCurrentInputConnection(); + if (!isConnected()) { + return false; + } + return mIC.performPrivateCommand(action, data); + } + public int getExpectedSelectionStart() { return mExpectedSelStart; } @@ -920,8 +917,6 @@ public final class RichInputConnection { } } - private boolean mCursorAnchorInfoMonitorEnabled = false; - /** * Requests the editor to call back {@link InputMethodManager#updateCursorAnchorInfo}. * @param enableMonitor {@code true} to request the editor to call back the method whenever the @@ -936,22 +931,10 @@ public final class RichInputConnection { public boolean requestCursorUpdates(final boolean enableMonitor, final boolean requestImmediateCallback) { mIC = mParent.getCurrentInputConnection(); - final boolean scheduled; - if (null != mIC) { - scheduled = InputConnectionCompatUtils.requestCursorUpdates(mIC, enableMonitor, - requestImmediateCallback); - } else { - scheduled = false; + if (!isConnected()) { + return false; } - mCursorAnchorInfoMonitorEnabled = (scheduled && enableMonitor); - return scheduled; - } - - /** - * @return {@code true} if the application reported that the monitor mode of - * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is currently enabled. - */ - public boolean isCursorAnchorInfoMonitorEnabled() { - return mCursorAnchorInfoMonitorEnabled; + return InputConnectionCompatUtils.requestCursorUpdates( + mIC, enableMonitor, requestImmediateCallback); } } diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java index 7cf4eff92..ef946c8bc 100644 --- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java +++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java @@ -16,10 +16,12 @@ 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; +import android.inputmethodservice.InputMethodService; +import android.os.AsyncTask; import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; @@ -28,20 +30,31 @@ import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; +import com.android.inputmethod.latin.utils.LanguageOnSpacebarUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * 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 static final boolean DEBUG = false; private RichInputMethodManager() { // This utility class is not publicly instantiable. @@ -49,12 +62,12 @@ 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>> - mSubtypeListCacheWithImplicitlySelectedSubtypes = new HashMap<>(); - final HashMap<InputMethodInfo, List<InputMethodSubtype>> - mSubtypeListCacheWithoutImplicitlySelectedSubtypes = new HashMap<>(); + private RichInputMethodSubtype mCurrentRichInputMethodSubtype; + private InputMethodInfo mShortcutInputMethodInfo; + private InputMethodSubtype mShortcutSubtype; private static final int INDEX_NOT_FOUND = -1; @@ -82,20 +95,24 @@ public final class RichInputMethodManager { return; } mImmWrapper = new InputMethodManagerCompatWrapper(context); + mContext = context; mInputMethodInfoCache = new InputMethodInfoCache( mImmWrapper.mImm, context.getPackageName()); // Initialize additional subtypes. SubtypeLocaleUtils.init(context); - final InputMethodSubtype[] additionalSubtypes = getAdditionalSubtypes(context); - setAdditionalInputMethodSubtypes(additionalSubtypes); + final InputMethodSubtype[] additionalSubtypes = getAdditionalSubtypes(); + mImmWrapper.mImm.setAdditionalInputMethodSubtypes( + getInputMethodIdOfThisIme(), additionalSubtypes); + + // Initialize the current input method subtype and the shortcut IME. + refreshSubtypeCaches(); } - public InputMethodSubtype[] getAdditionalSubtypes(final Context context) { - SubtypeLocaleUtils.init(context); - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + public InputMethodSubtype[] getAdditionalSubtypes() { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes( - prefs, context.getResources()); + prefs, mContext.getResources()); return AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefAdditionalSubtypes); } @@ -212,33 +229,57 @@ public final class RichInputMethodManager { private final InputMethodManager mImm; private final String mImePackageName; - private InputMethodInfo mCachedValue; + private InputMethodInfo mCachedThisImeInfo; + private final HashMap<InputMethodInfo, List<InputMethodSubtype>> + mCachedSubtypeListWithImplicitlySelected; + private final HashMap<InputMethodInfo, List<InputMethodSubtype>> + mCachedSubtypeListOnlyExplicitlySelected; public InputMethodInfoCache(final InputMethodManager imm, final String imePackageName) { mImm = imm; mImePackageName = imePackageName; + mCachedSubtypeListWithImplicitlySelected = new HashMap<>(); + mCachedSubtypeListOnlyExplicitlySelected = new HashMap<>(); } - public synchronized InputMethodInfo get() { - if (mCachedValue != null) { - return mCachedValue; + public synchronized InputMethodInfo getInputMethodOfThisIme() { + if (mCachedThisImeInfo != null) { + return mCachedThisImeInfo; } for (final InputMethodInfo imi : mImm.getInputMethodList()) { if (imi.getPackageName().equals(mImePackageName)) { - mCachedValue = imi; + mCachedThisImeInfo = imi; return imi; } } throw new RuntimeException("Input method id for " + mImePackageName + " not found."); } + public synchronized List<InputMethodSubtype> getEnabledInputMethodSubtypeList( + final InputMethodInfo imi, final boolean allowsImplicitlySelectedSubtypes) { + final HashMap<InputMethodInfo, List<InputMethodSubtype>> cache = + allowsImplicitlySelectedSubtypes + ? mCachedSubtypeListWithImplicitlySelected + : mCachedSubtypeListOnlyExplicitlySelected; + final List<InputMethodSubtype> cachedList = cache.get(imi); + if (cachedList != null) { + return cachedList; + } + final List<InputMethodSubtype> result = mImm.getEnabledInputMethodSubtypeList( + imi, allowsImplicitlySelectedSubtypes); + cache.put(imi, result); + return result; + } + public synchronized void clear() { - mCachedValue = null; + mCachedThisImeInfo = null; + mCachedSubtypeListWithImplicitlySelected.clear(); + mCachedSubtypeListOnlyExplicitlySelected.clear(); } } public InputMethodInfo getInputMethodInfoOfThisIme() { - return mInputMethodInfoCache.get(); + return mInputMethodInfoCache.getInputMethodOfThisIme(); } public String getInputMethodIdOfThisIme() { @@ -246,24 +287,20 @@ public final class RichInputMethodManager { } public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) { - return checkIfSubtypeBelongsToImeAndEnabled(getInputMethodInfoOfThisIme(), subtype); + return checkIfSubtypeBelongsToList(subtype, + getEnabledInputMethodSubtypeList( + getInputMethodInfoOfThisIme(), + true /* allowsImplicitlySelectedSubtypes */)); } public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled( final InputMethodSubtype subtype) { final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype); - final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList( - subtype, getMyEnabledInputMethodSubtypeList( - false /* allowsImplicitlySelectedSubtypes */)); + final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList(subtype, + getMyEnabledInputMethodSubtypeList(false /* allowsImplicitlySelectedSubtypes */)); return subtypeEnabled && !subtypeExplicitlyEnabled; } - public boolean checkIfSubtypeBelongsToImeAndEnabled(final InputMethodInfo imi, - final InputMethodSubtype subtype) { - return checkIfSubtypeBelongsToList(subtype, getEnabledInputMethodSubtypeList(imi, - true /* allowsImplicitlySelectedSubtypes */)); - } - private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype, final List<InputMethodSubtype> subtypes) { return getSubtypeIndexInList(subtype, subtypes) != INDEX_NOT_FOUND; @@ -281,26 +318,40 @@ public final class RichInputMethodManager { return INDEX_NOT_FOUND; } - public boolean checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype) { - return getSubtypeIndexInIme(subtype, getInputMethodInfoOfThisIme()) != INDEX_NOT_FOUND; + public void onSubtypeChanged(@Nonnull final InputMethodSubtype newSubtype) { + updateCurrentSubtype(newSubtype); + updateShortcutIme(); + if (DEBUG) { + Log.w(TAG, "onSubtypeChanged: " + mCurrentRichInputMethodSubtype.getNameForLogging()); + } } - private static int getSubtypeIndexInIme(final InputMethodSubtype subtype, - final InputMethodInfo imi) { - final int count = imi.getSubtypeCount(); - for (int index = 0; index < count; index++) { - final InputMethodSubtype ims = imi.getSubtypeAt(index); - if (ims.equals(subtype)) { - return index; - } + private static RichInputMethodSubtype sForcedSubtypeForTesting = null; + + @UsedForTesting + static void forceSubtype(@Nonnull final InputMethodSubtype subtype) { + sForcedSubtypeForTesting = RichInputMethodSubtype.getRichInputMethodSubtype(subtype); + } + + @Nonnull + public Locale getCurrentSubtypeLocale() { + if (null != sForcedSubtypeForTesting) { + return sForcedSubtypeForTesting.getLocale(); } - return INDEX_NOT_FOUND; + return getCurrentSubtype().getLocale(); } - public InputMethodSubtype getCurrentInputMethodSubtype( - final InputMethodSubtype defaultSubtype) { - final InputMethodSubtype currentSubtype = mImmWrapper.mImm.getCurrentInputMethodSubtype(); - return (currentSubtype != null) ? currentSubtype : defaultSubtype; + @Nonnull + public RichInputMethodSubtype getCurrentSubtype() { + if (null != sForcedSubtypeForTesting) { + return sForcedSubtypeForTesting; + } + return mCurrentRichInputMethodSubtype; + } + + + public String getCombiningRulesExtraValueOfCurrentSubtype() { + return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype()); } public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) { @@ -343,7 +394,6 @@ public final class RichInputMethodManager { // subtypes should be counted as well. if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) { ++filteredImisCount; - continue; } } @@ -388,27 +438,19 @@ public final class RichInputMethodManager { getInputMethodIdOfThisIme(), subtypes); // Clear the cache so that we go read the {@link InputMethodInfo} of this IME and list of // subtypes again next time. - clearSubtypeCaches(); + refreshSubtypeCaches(); } private List<InputMethodSubtype> getEnabledInputMethodSubtypeList(final InputMethodInfo imi, final boolean allowsImplicitlySelectedSubtypes) { - final HashMap<InputMethodInfo, List<InputMethodSubtype>> cache = - allowsImplicitlySelectedSubtypes - ? mSubtypeListCacheWithImplicitlySelectedSubtypes - : mSubtypeListCacheWithoutImplicitlySelectedSubtypes; - final List<InputMethodSubtype> cachedList = cache.get(imi); - if (null != cachedList) return cachedList; - final List<InputMethodSubtype> result = mImmWrapper.mImm.getEnabledInputMethodSubtypeList( + return mInputMethodInfoCache.getEnabledInputMethodSubtypeList( imi, allowsImplicitlySelectedSubtypes); - cache.put(imi, result); - return result; } - public void clearSubtypeCaches() { - mSubtypeListCacheWithImplicitlySelectedSubtypes.clear(); - mSubtypeListCacheWithoutImplicitlySelectedSubtypes.clear(); + public void refreshSubtypeCaches() { mInputMethodInfoCache.clear(); + updateCurrentSubtype(mImmWrapper.mImm.getCurrentInputMethodSubtype()); + updateShortcutIme(); } public boolean shouldOfferSwitchingToNextInputMethod(final IBinder binder, @@ -421,4 +463,109 @@ public final class RichInputMethodManager { } return mImmWrapper.shouldOfferSwitchingToNextInputMethod(binder); } + + public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes() { + final Locale systemLocale = mContext.getResources().getConfiguration().locale; + final Set<InputMethodSubtype> enabledSubtypesOfEnabledImes = new HashSet<>(); + final InputMethodManager inputMethodManager = getInputMethodManager(); + final List<InputMethodInfo> enabledInputMethodInfoList = + inputMethodManager.getEnabledInputMethodList(); + for (final InputMethodInfo info : enabledInputMethodInfoList) { + final List<InputMethodSubtype> enabledSubtypes = + inputMethodManager.getEnabledInputMethodSubtypeList( + info, true /* allowsImplicitlySelectedSubtypes */); + if (enabledSubtypes.isEmpty()) { + // An IME with no subtypes is found. + return false; + } + enabledSubtypesOfEnabledImes.addAll(enabledSubtypes); + } + for (final InputMethodSubtype subtype : enabledSubtypesOfEnabledImes) { + if (!subtype.isAuxiliary() && !subtype.getLocale().isEmpty() + && !systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) { + return false; + } + } + return true; + } + + private void updateCurrentSubtype(@Nullable final InputMethodSubtype subtype) { + mCurrentRichInputMethodSubtype = RichInputMethodSubtype.getRichInputMethodSubtype(subtype); + } + + private void updateShortcutIme() { + if (DEBUG) { + Log.d(TAG, "Update shortcut IME from : " + + (mShortcutInputMethodInfo == null + ? "<null>" : mShortcutInputMethodInfo.getId()) + ", " + + (mShortcutSubtype == null ? "<null>" : ( + mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode()))); + } + final RichInputMethodSubtype richSubtype = mCurrentRichInputMethodSubtype; + final boolean implicitlyEnabledSubtype = checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled( + richSubtype.getRawSubtype()); + final Locale systemLocale = mContext.getResources().getConfiguration().locale; + LanguageOnSpacebarUtils.onSubtypeChanged( + richSubtype, implicitlyEnabledSubtype, systemLocale); + LanguageOnSpacebarUtils.setEnabledSubtypes(getMyEnabledInputMethodSubtypeList( + true /* allowsImplicitlySelectedSubtypes */)); + + // TODO: Update an icon for shortcut IME + final Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts = + getInputMethodManager().getShortcutInputMethodsAndSubtypes(); + mShortcutInputMethodInfo = null; + mShortcutSubtype = null; + for (final InputMethodInfo imi : shortcuts.keySet()) { + final List<InputMethodSubtype> subtypes = shortcuts.get(imi); + // TODO: Returns the first found IMI for now. Should handle all shortcuts as + // appropriate. + mShortcutInputMethodInfo = imi; + // TODO: Pick up the first found subtype for now. Should handle all subtypes + // as appropriate. + mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null; + break; + } + if (DEBUG) { + Log.d(TAG, "Update shortcut IME to : " + + (mShortcutInputMethodInfo == null + ? "<null>" : mShortcutInputMethodInfo.getId()) + ", " + + (mShortcutSubtype == null ? "<null>" : ( + mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode()))); + } + } + + public void switchToShortcutIme(final InputMethodService context) { + if (mShortcutInputMethodInfo == null) { + return; + } + + final String imiId = mShortcutInputMethodInfo.getId(); + switchToTargetIME(imiId, mShortcutSubtype, context); + } + + private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype, + final InputMethodService context) { + final IBinder token = context.getWindow().getWindow().getAttributes().token; + if (token == null) { + return; + } + final InputMethodManager imm = getInputMethodManager(); + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + imm.setInputMethodAndSubtype(token, imiId, subtype); + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + public boolean isShortcutImeReady() { + if (mShortcutInputMethodInfo == null) { + return false; + } + if (mShortcutSubtype == null) { + return true; + } + return true; + } } 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..9d7849ffc --- /dev/null +++ b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java @@ -0,0 +1,227 @@ +/* + * 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 static com.android.inputmethod.latin.common.Constants.Subtype.KEYBOARD_MODE; + +import android.util.Log; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; +import com.android.inputmethod.latin.common.Constants; +import com.android.inputmethod.latin.common.LocaleUtils; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; + +import java.util.Locale; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Enrichment class for InputMethodSubtype to enable concurrent multi-lingual input. + * + * Right now, this returns the extra value of its primary subtype. + */ +// non final for easy mocking. +public class RichInputMethodSubtype { + private static final String TAG = RichInputMethodSubtype.class.getSimpleName(); + + @Nonnull + private final InputMethodSubtype mSubtype; + @Nonnull + private final Locale mLocale; + + public RichInputMethodSubtype(@Nonnull final InputMethodSubtype subtype) { + mSubtype = subtype; + mLocale = LocaleUtils.constructLocaleFromString(mSubtype.getLocale()); + } + + // Extra values are determined by the primary subtype. This is probably right, but + // we may have to revisit this later. + public String getExtraValueOf(@Nonnull 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() { + 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. + @Nonnull + public String getFullDisplayName() { + if (isNoLanguage()) { + return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype); + } + return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(mSubtype.getLocale()); + } + + // Get the RichInputMethodSubtype's middle display name in its locale. + @Nonnull + public String getMiddleDisplayName() { + if (isNoLanguage()) { + return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype); + } + return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(mSubtype.getLocale()); + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof RichInputMethodSubtype)) { + return false; + } + final RichInputMethodSubtype other = (RichInputMethodSubtype)o; + return mSubtype.equals(other.mSubtype) && mLocale.equals(other.mLocale); + } + + @Override + public int hashCode() { + return mSubtype.hashCode() + mLocale.hashCode(); + } + + @Override + public String toString() { + return "Multi-lingual subtype: " + mSubtype + ", " + mLocale; + } + + @Nonnull + public Locale getLocale() { + return mLocale; + } + + public boolean isRtlSubtype() { + // The subtype is considered RTL if the language of the main subtype is RTL. + return LocaleUtils.isRtlLanguage(mLocale); + } + + // TODO: remove this method + @Nonnull + public InputMethodSubtype getRawSubtype() { return mSubtype; } + + @Nonnull + public String getKeyboardLayoutSetName() { + return SubtypeLocaleUtils.getKeyboardLayoutSetName(mSubtype); + } + + public static RichInputMethodSubtype getRichInputMethodSubtype( + @Nullable final InputMethodSubtype subtype) { + if (subtype == null) { + return getNoLanguageSubtype(); + } else { + return new RichInputMethodSubtype(subtype); + } + } + + // Dummy no language QWERTY subtype. See {@link R.xml.method}. + private static final int SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE = 0xdde0bfd3; + private static final String EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE = + "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY + + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE + + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE + + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE; + @Nonnull + 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)); + // 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; + @Nonnull + 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)); + private static RichInputMethodSubtype sNoLanguageSubtype; + private static RichInputMethodSubtype sEmojiSubtype; + + @Nonnull + public static RichInputMethodSubtype getNoLanguageSubtype() { + RichInputMethodSubtype noLanguageSubtype = sNoLanguageSubtype; + if (noLanguageSubtype == null) { + final InputMethodSubtype rawNoLanguageSubtype = RichInputMethodManager.getInstance() + .findSubtypeByLocaleAndKeyboardLayoutSet( + SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY); + if (rawNoLanguageSubtype != null) { + noLanguageSubtype = new RichInputMethodSubtype(rawNoLanguageSubtype); + } + } + if (noLanguageSubtype != null) { + sNoLanguageSubtype = noLanguageSubtype; + return noLanguageSubtype; + } + Log.w(TAG, "Can't find any language with QWERTY subtype"); + Log.w(TAG, "No input method subtype found; returning dummy subtype: " + + DUMMY_NO_LANGUAGE_SUBTYPE); + return DUMMY_NO_LANGUAGE_SUBTYPE; + } + + @Nonnull + public static RichInputMethodSubtype getEmojiSubtype() { + RichInputMethodSubtype emojiSubtype = sEmojiSubtype; + if (emojiSubtype == null) { + final InputMethodSubtype rawEmojiSubtype = RichInputMethodManager.getInstance() + .findSubtypeByLocaleAndKeyboardLayoutSet( + SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI); + if (rawEmojiSubtype != null) { + emojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype); + } + } + if (emojiSubtype != null) { + sEmojiSubtype = emojiSubtype; + return emojiSubtype; + } + Log.w(TAG, "Can't find emoji subtype"); + Log.w(TAG, "No input method subtype found; returning dummy subtype: " + + DUMMY_EMOJI_SUBTYPE); + return DUMMY_EMOJI_SUBTYPE; + } +} diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java deleted file mode 100644 index a725e1611..000000000 --- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright (C) 2010 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 static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.REQ_NETWORK_CONNECTIVITY; - -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.inputmethodservice.InputMethodService; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.AsyncTask; -import android.os.IBinder; -import android.util.Log; -import android.view.inputmethod.InputMethodInfo; -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.InputMethodSubtype; - -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.define.DebugFlags; -import com.android.inputmethod.latin.utils.LocaleUtils; -import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; - -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -public final class SubtypeSwitcher { - private static boolean DBG = DebugFlags.DEBUG_ENABLED; - private static final String TAG = SubtypeSwitcher.class.getSimpleName(); - - private static final SubtypeSwitcher sInstance = new SubtypeSwitcher(); - - private /* final */ RichInputMethodManager mRichImm; - private /* final */ Resources mResources; - - private final LanguageOnSpacebarHelper mLanguageOnSpacebarHelper = - new LanguageOnSpacebarHelper(); - private InputMethodInfo mShortcutInputMethodInfo; - private InputMethodSubtype mShortcutSubtype; - private InputMethodSubtype mNoLanguageSubtype; - private InputMethodSubtype mEmojiSubtype; - private boolean mIsNetworkConnected; - - private static final String KEYBOARD_MODE = "keyboard"; - // Dummy no language QWERTY subtype. See {@link R.xml.method}. - private static final int SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE = 0xdde0bfd3; - private static final String EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE = - "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY - + "," + 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( - 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); - // 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 = - 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); - - public static SubtypeSwitcher getInstance() { - return sInstance; - } - - public static void init(final Context context) { - SubtypeLocaleUtils.init(context); - RichInputMethodManager.init(context); - sInstance.initialize(context); - } - - private SubtypeSwitcher() { - // Intentional empty constructor for singleton. - } - - private void initialize(final Context context) { - if (mResources != null) { - return; - } - mResources = context.getResources(); - mRichImm = RichInputMethodManager.getInstance(); - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService( - Context.CONNECTIVITY_SERVICE); - - final NetworkInfo info = connectivityManager.getActiveNetworkInfo(); - mIsNetworkConnected = (info != null && info.isConnected()); - - onSubtypeChanged(getCurrentSubtype()); - updateParametersOnStartInputView(); - } - - /** - * Update parameters which are changed outside LatinIME. This parameters affect UI so that they - * should be updated every time onStartInputView is called. - */ - public void updateParametersOnStartInputView() { - final List<InputMethodSubtype> enabledSubtypesOfThisIme = - mRichImm.getMyEnabledInputMethodSubtypeList(true); - mLanguageOnSpacebarHelper.updateEnabledSubtypes(enabledSubtypesOfThisIme); - updateShortcutIME(); - } - - private void updateShortcutIME() { - if (DBG) { - Log.d(TAG, "Update shortcut IME from : " - + (mShortcutInputMethodInfo == null - ? "<null>" : mShortcutInputMethodInfo.getId()) + ", " - + (mShortcutSubtype == null ? "<null>" : ( - mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode()))); - } - // TODO: Update an icon for shortcut IME - final Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts = - mRichImm.getInputMethodManager().getShortcutInputMethodsAndSubtypes(); - mShortcutInputMethodInfo = null; - mShortcutSubtype = null; - for (final InputMethodInfo imi : shortcuts.keySet()) { - final List<InputMethodSubtype> subtypes = shortcuts.get(imi); - // TODO: Returns the first found IMI for now. Should handle all shortcuts as - // appropriate. - mShortcutInputMethodInfo = imi; - // TODO: Pick up the first found subtype for now. Should handle all subtypes - // as appropriate. - mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null; - break; - } - if (DBG) { - Log.d(TAG, "Update shortcut IME to : " - + (mShortcutInputMethodInfo == null - ? "<null>" : mShortcutInputMethodInfo.getId()) + ", " - + (mShortcutSubtype == null ? "<null>" : ( - mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode()))); - } - } - - // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function. - public void onSubtypeChanged(final InputMethodSubtype newSubtype) { - if (DBG) { - Log.w(TAG, "onSubtypeChanged: " - + SubtypeLocaleUtils.getSubtypeNameForLogging(newSubtype)); - } - - 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(); - } - - //////////////////////////// - // Shortcut IME functions // - //////////////////////////// - - public void switchToShortcutIME(final InputMethodService context) { - if (mShortcutInputMethodInfo == null) { - return; - } - - final String imiId = mShortcutInputMethodInfo.getId(); - switchToTargetIME(imiId, mShortcutSubtype, context); - } - - private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype, - final InputMethodService context) { - final IBinder token = context.getWindow().getWindow().getAttributes().token; - if (token == null) { - return; - } - final InputMethodManager imm = mRichImm.getInputMethodManager(); - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - imm.setInputMethodAndSubtype(token, imiId, subtype); - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - public boolean isShortcutImeEnabled() { - updateShortcutIME(); - if (mShortcutInputMethodInfo == null) { - return false; - } - if (mShortcutSubtype == null) { - return true; - } - return mRichImm.checkIfSubtypeBelongsToImeAndEnabled( - mShortcutInputMethodInfo, mShortcutSubtype); - } - - public boolean isShortcutImeReady() { - updateShortcutIME(); - if (mShortcutInputMethodInfo == null) { - return false; - } - if (mShortcutSubtype == null) { - return true; - } - if (mShortcutSubtype.containsExtraValueKey(REQ_NETWORK_CONNECTIVITY)) { - return mIsNetworkConnected; - } - return true; - } - - public void onNetworkStateChanged(final Intent intent) { - final boolean noConnection = intent.getBooleanExtra( - ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); - mIsNetworkConnected = !noConnection; - - KeyboardSwitcher.getInstance().onNetworkStateChanged(); - } - - ////////////////////////////////// - // Subtype Switching functions // - ////////////////////////////////// - - public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) { - return mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(subtype); - } - - public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes() { - final Locale systemLocale = mResources.getConfiguration().locale; - final Set<InputMethodSubtype> enabledSubtypesOfEnabledImes = new HashSet<>(); - final InputMethodManager inputMethodManager = mRichImm.getInputMethodManager(); - final List<InputMethodInfo> enabledInputMethodInfoList = - inputMethodManager.getEnabledInputMethodList(); - for (final InputMethodInfo info : enabledInputMethodInfoList) { - final List<InputMethodSubtype> enabledSubtypes = - inputMethodManager.getEnabledInputMethodSubtypeList( - info, true /* allowsImplicitlySelectedSubtypes */); - if (enabledSubtypes.isEmpty()) { - // An IME with no subtypes is found. - return false; - } - enabledSubtypesOfEnabledImes.addAll(enabledSubtypes); - } - for (final InputMethodSubtype subtype : enabledSubtypesOfEnabledImes) { - if (!subtype.isAuxiliary() && !subtype.getLocale().isEmpty() - && !systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) { - return false; - } - } - return true; - } - - private static InputMethodSubtype sForcedSubtypeForTesting = null; - @UsedForTesting - void forceSubtype(final InputMethodSubtype subtype) { - sForcedSubtypeForTesting = subtype; - } - - public Locale getCurrentSubtypeLocale() { - if (null != sForcedSubtypeForTesting) { - return LocaleUtils.constructLocaleFromString(sForcedSubtypeForTesting.getLocale()); - } - return SubtypeLocaleUtils.getSubtypeLocale(getCurrentSubtype()); - } - - public InputMethodSubtype getCurrentSubtype() { - if (null != sForcedSubtypeForTesting) { - return sForcedSubtypeForTesting; - } - return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype()); - } - - public InputMethodSubtype getNoLanguageSubtype() { - if (mNoLanguageSubtype == null) { - mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( - SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY); - } - if (mNoLanguageSubtype != null) { - return mNoLanguageSubtype; - } - Log.w(TAG, "Can't find any language with QWERTY subtype"); - Log.w(TAG, "No input method subtype found; returning dummy subtype: " - + DUMMY_NO_LANGUAGE_SUBTYPE); - return DUMMY_NO_LANGUAGE_SUBTYPE; - } - - public InputMethodSubtype getEmojiSubtype() { - if (mEmojiSubtype == null) { - mEmojiSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( - SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI); - } - if (mEmojiSubtype != null) { - return mEmojiSubtype; - } - Log.w(TAG, "Can't find emoji subtype"); - Log.w(TAG, "No input method subtype found; returning dummy subtype: " - + DUMMY_EMOJI_SUBTYPE); - return DUMMY_EMOJI_SUBTYPE; - } - - public String getCombiningRulesExtraValueOfCurrentSubtype() { - return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype()); - } -} diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index b03818c1d..7ccefd2dd 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -18,18 +18,25 @@ package com.android.inputmethod.latin; import android.text.TextUtils; -import com.android.inputmethod.keyboard.ProximityInfo; +import static com.android.inputmethod.latin.define.DecoderSpecificConstants.SHOULD_AUTO_CORRECT_USING_NON_WHITE_LISTED_SUGGESTION; +import static com.android.inputmethod.latin.define.DecoderSpecificConstants.SHOULD_REMOVE_PREVIOUSLY_REJECTED_SUGGESTION; + +import com.android.inputmethod.keyboard.Keyboard; 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; +import javax.annotation.Nonnull; + /** * This class loads a dictionary and provides a list of suggestions for a given sequence of * characters. This includes corrections and completions. @@ -49,34 +56,55 @@ 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; + private float mPlausibilityThreshold; public Suggest(final DictionaryFacilitator dictionaryFacilitator) { mDictionaryFacilitator = dictionaryFacilitator; } - public Locale getLocale() { - return mDictionaryFacilitator.getLocale(); - } - + /** + * Set the normalized-score threshold for a suggestion to be considered strong enough that we + * will auto-correct to this. + * @param threshold the threshold + */ public void setAutoCorrectionThreshold(final float threshold) { mAutoCorrectionThreshold = threshold; } + /** + * Set the normalized-score threshold for what we consider a "plausible" suggestion, in + * the same dimension as the auto-correction threshold. + * @param threshold the threshold + */ + public void setPlausibilityThreshold(final float threshold) { + mPlausibilityThreshold = threshold; + } + public interface OnGetSuggestedWordsCallback { public void onGetSuggestedWords(final SuggestedWords suggestedWords); } public void getSuggestedWords(final WordComposer wordComposer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final NgramContext ngramContext, final Keyboard keyboard, 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, keyboard, settingsValuesForSuggestion, inputStyle, sequenceNumber, callback); } else { - getSuggestedWordsForNonBatchInput(wordComposer, prevWordsInfo, proximityInfo, + getSuggestedWordsForNonBatchInput(wordComposer, ngramContext, keyboard, settingsValuesForSuggestion, inputStyle, isCorrectionEnabled, sequenceNumber, callback); } @@ -84,7 +112,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,16 +124,19 @@ 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); } } return suggestionsContainer; } - private static String getWhitelistedWordOrNull(final ArrayList<SuggestedWordInfo> suggestions) { + private static SuggestedWordInfo getWhitelistedWordInfoOrNull( + @Nonnull final ArrayList<SuggestedWordInfo> suggestions) { if (suggestions.isEmpty()) { return null; } @@ -113,75 +144,124 @@ public final class Suggest { if (!firstSuggestedWordInfo.isKindOf(SuggestedWordInfo.KIND_WHITELIST)) { return null; } - return firstSuggestedWordInfo.mWord; + return firstSuggestedWordInfo; } // 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 Keyboard keyboard, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int inputStyleIfNotPrediction, final boolean isCorrectionEnabled, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { - final String typedWord = wordComposer.getTypedWord(); - final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(typedWord); + final String typedWordString = wordComposer.getTypedWord(); + final int trailingSingleQuotesCount = + StringUtils.getTrailingSingleQuotesCount(typedWordString); final String consideredWord = trailingSingleQuotesCount > 0 - ? typedWord.substring(0, typedWord.length() - trailingSingleQuotesCount) - : typedWord; + ? typedWordString.substring(0, typedWordString.length() - trailingSingleQuotesCount) + : typedWordString; final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( - wordComposer, prevWordsInfo, proximityInfo, settingsValuesForSuggestion, - SESSION_ID_TYPING); + wordComposer.getComposedDataSnapshot(), ngramContext, keyboard, + settingsValuesForSuggestion, SESSION_ID_TYPING, inputStyleIfNotPrediction); + final Locale locale = mDictionaryFacilitator.getLocale(); final ArrayList<SuggestedWordInfo> suggestionsContainer = getTransformedSuggestedWordInfoList(wordComposer, suggestionResults, - trailingSingleQuotesCount); - final boolean didRemoveTypedWord = - SuggestedWordInfo.removeDups(wordComposer.getTypedWord(), suggestionsContainer); + trailingSingleQuotesCount, locale); - final String whitelistedWord = getWhitelistedWordOrNull(suggestionsContainer); + boolean foundInDictionary = false; + Dictionary sourceDictionaryOfRemovedWord = null; + for (final SuggestedWordInfo info : suggestionsContainer) { + // Search for the best dictionary, defined as the first one with the highest match + // quality we can find. + if (!foundInDictionary && typedWordString.equals(info.mWord)) { + // Use this source if the old match had lower quality than this match + sourceDictionaryOfRemovedWord = info.mSourceDict; + foundInDictionary = true; + break; + } + } + + final int firstOcurrenceOfTypedWordInSuggestions = + SuggestedWordInfo.removeDups(typedWordString, suggestionsContainer); + + final SuggestedWordInfo whitelistedWordInfo = + getWhitelistedWordInfoOrNull(suggestionsContainer); + final String whitelistedWord = whitelistedWordInfo == null + ? null : whitelistedWordInfo.mWord; final boolean resultsArePredictions = !wordComposer.isComposingWord(); - // We allow auto-correction if we have a whitelisted word, or if the word had more than - // one char and was not suggested. - final boolean allowsToBeAutoCorrected = (null != whitelistedWord) - || (consideredWord.length() > 1 && !didRemoveTypedWord); + // We allow auto-correction if whitelisting is not required or the word is whitelisted, + // or if the word had more than one char and was not suggested. + final boolean allowsToBeAutoCorrected = + (SHOULD_AUTO_CORRECT_USING_NON_WHITE_LISTED_SUGGESTION || whitelistedWord != null) + || (consideredWord.length() > 1 && (sourceDictionaryOfRemovedWord == null)); 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 (suggestionResults.mFirstSuggestionExceedsConfidenceThreshold + && firstOcurrenceOfTypedWordInSuggestions != 0) { + hasAutoCorrection = true; + } else if (!AutoCorrectionUtils.suggestionExceedsThreshold( + 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)) { - suggestionsContainer.add(0, new SuggestedWordInfo(typedWord, - SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED, - Dictionary.DICTIONARY_USER_TYPED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); + final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo(typedWordString, + "" /* prevWordsContext */, SuggestedWordInfo.MAX_SCORE, + SuggestedWordInfo.KIND_TYPED, + null == sourceDictionaryOfRemovedWord ? Dictionary.DICTIONARY_USER_TYPED + : sourceDictionaryOfRemovedWord, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); + if (!TextUtils.isEmpty(typedWordString)) { + suggestionsContainer.add(0, typedWordInfo); } final ArrayList<SuggestedWordInfo> suggestionsList; if (DBG && !suggestionsContainer.isEmpty()) { - suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, suggestionsContainer); + suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWordString, + suggestionsContainer); } else { suggestionsList = suggestionsContainer; } @@ -194,12 +274,12 @@ public final class Suggest { } else { inputStyle = inputStyleIfNotPrediction; } + + final boolean isTypedWordValid = firstOcurrenceOfTypedWordInSuggestions > -1 + || (!resultsArePredictions && !allowsToBeAutoCorrected); callback.onGetSuggestedWords(new SuggestedWords(suggestionsList, - suggestionResults.mRawSuggestions, - // TODO: this first argument is lying. If this is a whitelisted word which is an - // actual word, it says typedWordValid = false, which looks wrong. We should either - // rename the attribute or change the value. - !resultsArePredictions && !allowsToBeAutoCorrected /* typedWordValid */, + suggestionResults.mRawSuggestions, typedWordInfo, + isTypedWordValid, hasAutoCorrection /* willAutoCorrect */, false /* isObsoleteSuggestions */, inputStyle, sequenceNumber)); } @@ -207,13 +287,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 Keyboard keyboard, 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.getComposedDataSnapshot(), ngramContext, keyboard, + settingsValuesForSuggestion, SESSION_ID_GESTURE, inputStyle); + // For transforming words that don't come from a dictionary, because it's our best bet + final Locale locale = mDictionaryFacilitator.getLocale(); final ArrayList<SuggestedWordInfo> suggestionsContainer = new ArrayList<>(suggestionResults); final int suggestionsCount = suggestionsContainer.size(); @@ -222,22 +304,25 @@ 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 ? locale : wordlocale, isAllUpperCase, + isFirstCharCapitalized, 0 /* trailingSingleQuotesCount */); suggestionsContainer.set(i, transformedWordInfo); } } - if (suggestionsContainer.size() > 1 && TextUtils.equals(suggestionsContainer.get(0).mWord, - wordComposer.getRejectedBatchModeSuggestion())) { + if (SHOULD_REMOVE_PREVIOUSLY_REJECTED_SUGGESTION + && suggestionsContainer.size() > 1 + && TextUtils.equals(suggestionsContainer.get(0).mWord, + wordComposer.getRejectedBatchModeSuggestion())) { final SuggestedWordInfo rejected = suggestionsContainer.remove(0); suggestionsContainer.add(1, rejected); } 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); @@ -248,8 +333,12 @@ public final class Suggest { // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false). // Note that because this method is never used to get predictions, there is no need to // modify inputType such in getSuggestedWordsForNonBatchInput. + final SuggestedWordInfo pseudoTypedWordInfo = suggestionsContainer.isEmpty() ? null + : suggestionsContainer.get(0); + callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer, suggestionResults.mRawSuggestions, + pseudoTypedWordInfo, true /* typedWordValid */, false /* willAutoCorrect */, false /* isObsoleteSuggestions */, @@ -283,6 +372,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) { @@ -302,7 +426,8 @@ public final class Suggest { for (int i = quotesToAppend - 1; i >= 0; --i) { sb.appendCodePoint(Constants.CODE_SINGLE_QUOTE); } - return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKindAndFlags, + return new SuggestedWordInfo(sb.toString(), wordInfo.mPrevWordsContext, + wordInfo.mScore, wordInfo.mKindAndFlags, wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord, wordInfo.mAutoCommitFirstWordConfidence); } diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 1d221b77f..bcd4d5f69 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.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; +import javax.annotation.Nullable; + public class SuggestedWords { public static final int INDEX_OF_TYPED_WORD = 0; public static final int INDEX_OF_AUTO_CORRECTION = 1; @@ -45,11 +48,14 @@ 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( - EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false /* typedWordValid */, - false /* willAutoCorrect */, false /* isObsoleteSuggestions */, INPUT_STYLE_NONE); - - public final String mTypedWord; + @Nonnull + private static final SuggestedWords EMPTY = new SuggestedWords( + EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, null /* typedWord */, + false /* typedWordValid */, false /* willAutoCorrect */, + false /* isObsoleteSuggestions */, INPUT_STYLE_NONE, NOT_A_SEQUENCE_NUMBER); + + @Nullable + public final SuggestedWordInfo mTypedWordInfo; public final boolean mTypedWordValid; // Note: this INCLUDES cases where the word will auto-correct to itself. A good definition // of what this flag means would be "the top suggestion is strong enough to auto-correct", @@ -60,35 +66,14 @@ public class SuggestedWords { // INPUT_STYLE_* constants above. public final int mInputStyle; public final int mSequenceNumber; // Sequence number for auto-commit. + @Nonnull protected final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList; + @Nullable public final ArrayList<SuggestedWordInfo> mRawSuggestions; - public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, - final ArrayList<SuggestedWordInfo> rawSuggestions, - final boolean typedWordValid, - final boolean willAutoCorrect, - final boolean isObsoleteSuggestions, - final int inputStyle) { - this(suggestedWordInfoList, rawSuggestions, typedWordValid, willAutoCorrect, - isObsoleteSuggestions, inputStyle, NOT_A_SEQUENCE_NUMBER); - } - - public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, - final ArrayList<SuggestedWordInfo> rawSuggestions, - final boolean typedWordValid, - final boolean willAutoCorrect, - final boolean isObsoleteSuggestions, - final int inputStyle, - final int sequenceNumber) { - this(suggestedWordInfoList, rawSuggestions, - (suggestedWordInfoList.isEmpty() || isPrediction(inputStyle)) ? null - : suggestedWordInfoList.get(INDEX_OF_TYPED_WORD).mWord, - typedWordValid, willAutoCorrect, isObsoleteSuggestions, inputStyle, sequenceNumber); - } - - public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, - final ArrayList<SuggestedWordInfo> rawSuggestions, - final String typedWord, + public SuggestedWords(@Nonnull final ArrayList<SuggestedWordInfo> suggestedWordInfoList, + @Nullable final ArrayList<SuggestedWordInfo> rawSuggestions, + @Nullable final SuggestedWordInfo typedWordInfo, final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, @@ -101,7 +86,7 @@ public class SuggestedWords { mIsObsoleteSuggestions = isObsoleteSuggestions; mInputStyle = inputStyle; mSequenceNumber = sequenceNumber; - mTypedWord = typedWord; + mTypedWordInfo = typedWordInfo; } public boolean isEmpty() { @@ -113,6 +98,27 @@ 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 {@link SuggestedWordInfo} object for the typed word. + * @return The {@link SuggestedWordInfo} object for the typed word. + */ + public SuggestedWordInfo getTypedWordInfo() { + return mTypedWordInfo; + } + + /** * Get suggested word at <code>index</code>. * @param index The index of the suggested word. * @return The suggested word. @@ -142,6 +148,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,17 +202,20 @@ 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( - final String typedWord, final SuggestedWords previousSuggestions) { + @Nonnull final SuggestedWordInfo typedWordInfo, + @Nonnull final SuggestedWords previousSuggestions) { final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<>(); final HashSet<String> alreadySeen = new HashSet<>(); - suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE, - SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); - alreadySeen.add(typedWord.toString()); + suggestionsList.add(typedWordInfo); + alreadySeen.add(typedWordInfo.mWord); final int previousSize = previousSuggestions.size(); for (int index = 1; index < previousSize; index++) { final SuggestedWordInfo prevWordInfo = previousSuggestions.getInfo(index); @@ -217,7 +235,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; @@ -240,14 +259,17 @@ public class SuggestedWords { public static final int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000; public static final int KIND_FLAG_EXACT_MATCH = 0x40000000; public static final int KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION = 0x20000000; + public static final int KIND_FLAG_APPROPRIATE_FOR_AUTO_CORRECTION = 0x10000000; public final String mWord; + public final String mPrevWordsContext; // The completion info from the application. Null for suggestions that don't come from // the application (including keyboard-computed ones, so this is almost always null) public final CompletionInfo mApplicationSpecifiedCompletionInfo; public final int mScore; public final int mKindAndFlags; public final int mCodePointCount; + @Deprecated public final Dictionary mSourceDict; // For auto-commit. This keeps track of the index inside the touch coordinates array // passed to native code to get suggestions for a gesture that corresponds to the first @@ -261,6 +283,7 @@ public class SuggestedWords { /** * Create a new suggested word info. * @param word The string to suggest. + * @param prevWordsContext previous words context. * @param score A measure of how likely this suggestion is. * @param kindAndFlags The kind of suggestion, as one of the above KIND_* constants with * flags. @@ -268,10 +291,12 @@ public class SuggestedWords { * @param indexOfTouchPointOfSecondWord See mIndexOfTouchPointOfSecondWord. * @param autoCommitFirstWordConfidence See mAutoCommitFirstWordConfidence. */ - public SuggestedWordInfo(final String word, final int score, final int kindAndFlags, + public SuggestedWordInfo(final String word, final String prevWordsContext, + final int score, final int kindAndFlags, final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord, final int autoCommitFirstWordConfidence) { mWord = word; + mPrevWordsContext = prevWordsContext; mApplicationSpecifiedCompletionInfo = null; mScore = score; mKindAndFlags = kindAndFlags; @@ -288,6 +313,7 @@ public class SuggestedWords { */ public SuggestedWordInfo(final CompletionInfo applicationSpecifiedCompletion) { mWord = applicationSpecifiedCompletion.getText().toString(); + mPrevWordsContext = ""; mApplicationSpecifiedCompletionInfo = applicationSpecifiedCompletion; mScore = SuggestedWordInfo.MAX_SCORE; mKindAndFlags = SuggestedWordInfo.KIND_APP_DEFINED; @@ -321,6 +347,10 @@ public class SuggestedWords { return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0; } + public boolean isAprapreateForAutoCorrection() { + return (mKindAndFlags & KIND_FLAG_APPROPRIATE_FOR_AUTO_CORRECTION) != 0; + } + public void setDebugString(final String str) { if (null == str) throw new NullPointerException("Debug info is null"); mDebugString = str; @@ -330,6 +360,15 @@ public class SuggestedWords { return mDebugString; } + public String getWord() { + return mWord; + } + + @Deprecated + public Dictionary getSourceDictionary() { + return mSourceDict; + } + public int codePointAt(int i) { return mWord.codePointAt(i); } @@ -338,43 +377,49 @@ 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. - public static boolean removeDups(final String typedWord, - ArrayList<SuggestedWordInfo> candidates) { + /** + * This will always remove the higher index if a duplicate is found. + * + * @return position of typed word in the candidate list + */ + public static int removeDups( + @Nullable final String typedWord, + @Nonnull final ArrayList<SuggestedWordInfo> candidates) { if (candidates.isEmpty()) { - return false; + return -1; } - final boolean didRemoveTypedWord; + int firstOccurrenceOfWord = -1; if (!TextUtils.isEmpty(typedWord)) { - didRemoveTypedWord = removeSuggestedWordInfoFrom(typedWord, candidates, - -1 /* startIndexExclusive */); - } else { - didRemoveTypedWord = false; + firstOccurrenceOfWord = removeSuggestedWordInfoFromList( + typedWord, candidates, -1 /* startIndexExclusive */); } for (int i = 0; i < candidates.size(); ++i) { - removeSuggestedWordInfoFrom(candidates.get(i).mWord, candidates, - i /* startIndexExclusive */); + removeSuggestedWordInfoFromList( + candidates.get(i).mWord, candidates, i /* startIndexExclusive */); } - return didRemoveTypedWord; + return firstOccurrenceOfWord; } - private static boolean removeSuggestedWordInfoFrom(final String word, - final ArrayList<SuggestedWordInfo> candidates, final int startIndexExclusive) { - boolean didRemove = false; + private static int removeSuggestedWordInfoFromList( + @Nonnull final String word, + @Nonnull final ArrayList<SuggestedWordInfo> candidates, + final int startIndexExclusive) { + int firstOccurrenceOfWord = -1; for (int i = startIndexExclusive + 1; i < candidates.size(); ++i) { final SuggestedWordInfo previous = candidates.get(i); if (word.equals(previous.mWord)) { - didRemove = true; + if (firstOccurrenceOfWord == -1) { + firstOccurrenceOfWord = i; + } candidates.remove(i); --i; } } - return didRemove; + return firstOccurrenceOfWord; } } @@ -387,47 +432,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 - // we should only suggest replacements for this last word. - // TODO: make this work with languages without spaces. - public SuggestedWords getSuggestedWordsForLastWordOfPhraseGesture() { - final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>(); - for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { - final SuggestedWordInfo info = mSuggestedWordInfoList.get(i); - final int indexOfLastSpace = info.mWord.lastIndexOf(Constants.CODE_SPACE) + 1; - final String lastWord = info.mWord.substring(indexOfLastSpace); - newSuggestions.add(new SuggestedWordInfo(lastWord, info.mScore, info.mKindAndFlags, - info.mSourceDict, SuggestedWordInfo.NOT_AN_INDEX, - SuggestedWordInfo.NOT_A_CONFIDENCE)); - } - return new SuggestedWords(newSuggestions, null /* rawSuggestions */, mTypedWordValid, - mWillAutoCorrect, mIsObsoleteSuggestions, INPUT_STYLE_TAIL_BATCH); - } - /** * @return the {@link SuggestedWordInfo} which corresponds to the word that is originally * typed by the user. Otherwise returns {@code null}. Note that gesture input is not diff --git a/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java b/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java deleted file mode 100644 index 08785f3d9..000000000 --- a/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.text.style.SuggestionSpan; -import android.util.Log; - -import com.android.inputmethod.latin.define.DebugFlags; - -public final class SuggestionSpanPickedNotificationReceiver extends BroadcastReceiver { - private static final boolean DBG = DebugFlags.DEBUG_ENABLED; - private static final String TAG = - SuggestionSpanPickedNotificationReceiver.class.getSimpleName(); - - @Override - public void onReceive(Context context, Intent intent) { - if (SuggestionSpan.ACTION_SUGGESTION_PICKED.equals(intent.getAction())) { - if (DBG) { - final String before = intent.getStringExtra( - SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE); - final String after = intent.getStringExtra( - SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER); - Log.d(TAG, "Received notification picked: " + before + "," + after); - } - } - } -} diff --git a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java index 123ab208c..2a69d3650 100644 --- a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java +++ b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java @@ -17,16 +17,20 @@ package com.android.inputmethod.latin; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.Process; import android.util.Log; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; -import com.android.inputmethod.compat.IntentCompatUtils; +import com.android.inputmethod.dictionarypack.CommonPreferences; +import com.android.inputmethod.dictionarypack.DictionaryPackConstants; import com.android.inputmethod.keyboard.KeyboardLayoutSet; -import com.android.inputmethod.latin.setup.LauncherIconVisibilityManager; +import com.android.inputmethod.latin.setup.SetupActivity; import com.android.inputmethod.latin.utils.UncachedInputMethodManagerUtils; /** @@ -50,10 +54,6 @@ import com.android.inputmethod.latin.utils.UncachedInputMethodManagerUtils; * receiver and it checks whether the setup wizard's icon should be appeared or not on the launcher * depending on which partition this IME is installed. * - * When a multiuser account has been created, {@link Intent#ACTION_USER_INITIALIZE} is received - * by this receiver and it checks the whether the setup wizard's icon should be appeared or not on - * the launcher depending on which partition this IME is installed. - * * When the system locale has been changed, {@link Intent#ACTION_LOCALE_CHANGED} is received by * this receiver and the {@link KeyboardLayoutSet}'s cache is cleared. */ @@ -69,26 +69,25 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { // subtypes when the package is replaced. RichInputMethodManager.init(context); final RichInputMethodManager richImm = RichInputMethodManager.getInstance(); - final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes(context); + final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes(); richImm.setAdditionalInputMethodSubtypes(additionalSubtypes); - LauncherIconVisibilityManager.updateSetupWizardIconVisibility(context); + toggleAppIcon(context); + downloadLatestDictionaries(context); } else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) { Log.i(TAG, "Boot has been completed"); - LauncherIconVisibilityManager.updateSetupWizardIconVisibility(context); - } else if (IntentCompatUtils.is_ACTION_USER_INITIALIZE(intentAction)) { - Log.i(TAG, "User initialize"); - LauncherIconVisibilityManager.updateSetupWizardIconVisibility(context); + toggleAppIcon(context); } else if (Intent.ACTION_LOCALE_CHANGED.equals(intentAction)) { Log.i(TAG, "System locale changed"); KeyboardLayoutSet.onSystemLocaleChanged(); } // The process that hosts this broadcast receiver is invoked and remains alive even after - // 1) the package has been re-installed, 2) the device has just booted, + // 1) the package has been re-installed, + // 2) the device has just booted, // 3) a new user has been created. // There is no good reason to keep the process alive if this IME isn't a current IME. - final InputMethodManager imm = - (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); + final InputMethodManager imm = (InputMethodManager) + context.getSystemService(Context.INPUT_METHOD_SERVICE); // Called to check whether this IME has been triggered by the current user or not final boolean isInputMethodManagerValidForUserOfThisProcess = !imm.getInputMethodList().isEmpty(); @@ -100,4 +99,24 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { Process.killProcess(myPid); } } + + private void downloadLatestDictionaries(Context context) { + final Intent updateIntent = new Intent( + DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION); + context.sendBroadcast(updateIntent); + } + + private static void toggleAppIcon(final Context context) { + final int appInfoFlags = context.getApplicationInfo().flags; + final boolean isSystemApp = (appInfoFlags & ApplicationInfo.FLAG_SYSTEM) > 0; + if (Log.isLoggable(TAG, Log.INFO)) { + Log.i(TAG, "toggleAppIcon() : FLAG_SYSTEM = " + isSystemApp); + } + context.getPackageManager().setComponentEnabledSetting( + new ComponentName(context, SetupActivity.class), + isSystemApp + ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED + : PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP); + } } diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java index 21014b378..fe24ccfc2 100644 --- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java @@ -16,26 +16,25 @@ package com.android.inputmethod.latin; -import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.net.Uri; -import android.os.Build; import android.provider.UserDictionary.Words; import android.text.TextUtils; import android.util.Log; -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.compat.UserDictionaryCompatUtils; +import com.android.inputmethod.annotations.ExternallyReferenced; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import java.io.File; import java.util.Arrays; import java.util.Locale; +import javax.annotation.Nullable; + /** * An expandable dictionary that stores the words in the user dictionary provider into a binary * dictionary file to use it from native code. @@ -47,36 +46,26 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { private static final String USER_DICTIONARY_ALL_LANGUAGES = ""; private static final int HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY = 250; private static final int LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY = 160; - // Shortcut frequency is 0~15, with 15 = whitelist. We don't want user dictionary entries - // to auto-correct, so we set this to the highest frequency that won't, i.e. 14. - private static final int USER_DICT_SHORTCUT_FREQUENCY = 14; - private static final String[] PROJECTION_QUERY_WITH_SHORTCUT = new String[] { - Words.WORD, - Words.SHORTCUT, - Words.FREQUENCY, - }; - private static final String[] PROJECTION_QUERY_WITHOUT_SHORTCUT = new String[] { - Words.WORD, - Words.FREQUENCY, - }; + private static final String[] PROJECTION_QUERY = new String[] {Words.WORD, Words.FREQUENCY}; 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, - final boolean alsoUseMoreRestrictiveLocales, final File dictFile, final String name) { + final boolean alsoUseMoreRestrictiveLocales, + final File dictFile, final String name) { super(context, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_USER, dictFile); if (null == locale) throw new NullPointerException(); // Catch the error earlier 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,10 +90,13 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { reloadDictionaryIfRequired(); } - @UsedForTesting - public static UserBinaryDictionary getDictionary(final Context context, final Locale locale, - final File dictFile, final String dictNamePrefix) { - return new UserBinaryDictionary(context, locale, false /* alsoUseMoreRestrictiveLocales */, + // 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, @Nullable final String account) { + return new UserBinaryDictionary( + context, locale, false /* alsoUseMoreRestrictiveLocales */, dictFile, dictNamePrefix + NAME); } @@ -124,7 +116,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)"); @@ -167,24 +159,12 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { requestArguments = localeElements; } final String requestString = request.toString(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - try { - addWordsFromProjectionLocked(PROJECTION_QUERY_WITH_SHORTCUT, requestString, - requestArguments); - } catch (IllegalArgumentException e) { - // This may happen on some non-compliant devices where the declared API is JB+ but - // the SHORTCUT column is not present for some reason. - addWordsFromProjectionLocked(PROJECTION_QUERY_WITHOUT_SHORTCUT, requestString, - requestArguments); - } - } else { - addWordsFromProjectionLocked(PROJECTION_QUERY_WITHOUT_SHORTCUT, requestString, - requestArguments); - } + addWordsFromProjectionLocked(PROJECTION_QUERY, requestString, requestArguments); } private void addWordsFromProjectionLocked(final String[] query, String request, - final String[] requestArguments) throws IllegalArgumentException { + final String[] requestArguments) + throws IllegalArgumentException { Cursor cursor = null; try { cursor = mContext.getContentResolver().query( @@ -201,69 +181,33 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { } } - public static boolean isEnabled(final Context context) { - final ContentResolver cr = context.getContentResolver(); - final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI); - if (client != null) { - client.release(); - return true; - } else { - return false; - } - } - - /** - * Adds a word to the user dictionary and makes it persistent. - * - * @param context the context - * @param locale the locale - * @param word the word to add. If the word is capitalized, then the dictionary will - * recognize it as a capitalized word when searched. - */ - public static void addWordToUserDictionary(final Context context, final Locale locale, - final String word) { - // Update the user dictionary provider - UserDictionaryCompatUtils.addWord(context, word, - 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) { - final boolean hasShortcutColumn = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; if (cursor == null) return; if (cursor.moveToFirst()) { final int indexWord = cursor.getColumnIndex(Words.WORD); - final int indexShortcut = hasShortcutColumn ? cursor.getColumnIndex(Words.SHORTCUT) : 0; final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY); while (!cursor.isAfterLast()) { final String word = cursor.getString(indexWord); - final String shortcut = hasShortcutColumn ? cursor.getString(indexShortcut) : null; final int frequency = cursor.getInt(indexFrequency); final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency); // Safeguard against adding really long words. if (word.length() <= MAX_WORD_LENGTH) { runGCIfRequiredLocked(true /* mindsBlockByGC */); - addUnigramLocked(word, adjustedFrequency, null /* shortcutTarget */, - 0 /* shortcutFreq */, false /* isNotAWord */, - false /* isBlacklisted */, 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); - } + addUnigramLocked(word, adjustedFrequency, false /* isNotAWord */, + 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..8803edc88 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -16,11 +16,17 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.annotations.UsedForTesting; 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.CoordinateUtils; +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 com.android.inputmethod.latin.define.DecoderSpecificConstants; import java.util.ArrayList; import java.util.Collections; @@ -31,7 +37,7 @@ import javax.annotation.Nonnull; * A place to store the currently composing word with information such as adjacent key codes as well */ public final class WordComposer { - private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; + private static final int MAX_WORD_LENGTH = DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH; private static final boolean DBG = DebugFlags.DEBUG_ENABLED; public static final int CAPS_MODE_OFF = 0; @@ -48,7 +54,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 +93,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. @@ -95,8 +105,7 @@ public final class WordComposer { final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec; if (!nonNullCombiningSpec.equals(mCombiningSpec)) { mCombinerChain = new CombinerChain( - mCombinerChain.getComposingWordWithCombiningFeedback().toString(), - CombinerChain.createCombiners(nonNullCombiningSpec)); + mCombinerChain.getComposingWordWithCombiningFeedback().toString()); mCombiningSpec = nonNullCombiningSpec; } } @@ -127,43 +136,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 +158,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. @@ -256,31 +232,33 @@ public final class WordComposer { * @return true if the cursor is still inside the composing word, false otherwise. */ public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) { - // TODO: should uncommit the composing feedback - mCombinerChain.reset(); - int actualMoveAmountWithinWord = 0; + int actualMoveAmount = 0; int cursorPos = mCursorPositionWithinWord; // TODO: Don't make that copy. We can do this directly from mTypedWordCache. final int[] codePoints = StringUtils.toCodePointArray(mTypedWordCache); if (expectedMoveAmount >= 0) { // Moving the cursor forward for the expected amount or until the end of the word has // been reached, whichever comes first. - while (actualMoveAmountWithinWord < expectedMoveAmount && cursorPos < mCodePointSize) { - actualMoveAmountWithinWord += Character.charCount(codePoints[cursorPos]); + while (actualMoveAmount < expectedMoveAmount && cursorPos < codePoints.length) { + actualMoveAmount += Character.charCount(codePoints[cursorPos]); ++cursorPos; } } else { // Moving the cursor backward for the expected amount or until the start of the word // has been reached, whichever comes first. - while (actualMoveAmountWithinWord > expectedMoveAmount && cursorPos > 0) { + while (actualMoveAmount > expectedMoveAmount && cursorPos > 0) { --cursorPos; - actualMoveAmountWithinWord -= Character.charCount(codePoints[cursorPos]); + actualMoveAmount -= Character.charCount(codePoints[cursorPos]); } } // If the actual and expected amounts differ, we crossed the start or the end of the word // so the result would not be inside the composing word. - if (actualMoveAmountWithinWord != expectedMoveAmount) return false; + if (actualMoveAmount != expectedMoveAmount) { + return false; + } mCursorPositionWithinWord = cursorPos; + mCombinerChain.applyProcessedEvent(mCombinerChain.processEvent( + mEvents, Event.createCursorMovedEvent(cursorPos))); return true; } @@ -353,9 +331,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 +395,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 +416,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) { @@ -491,4 +468,14 @@ public final class WordComposer { public String getRejectedBatchModeSuggestion() { return mRejectedBatchModeSuggestion; } + + @UsedForTesting + void addInputPointerForTest(int index, int keyX, int keyY) { + mInputPointers.addPointerAt(index, keyX, keyY, 0, 0); + } + + @UsedForTesting + void setTypedWordCacheForTests(String typedWordCacheForTests) { + mTypedWordCache = typedWordCacheForTests; + } } 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 deleted file mode 100644 index a87785b1a..000000000 --- a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (C) 2013 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.debug; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.content.DialogInterface.OnClickListener; -import android.os.Environment; - -import com.android.inputmethod.latin.BinaryDictionaryFileDumper; -import com.android.inputmethod.latin.BinaryDictionaryGetter; -import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.makedict.DictionaryHeader; -import com.android.inputmethod.latin.utils.DialogUtils; -import com.android.inputmethod.latin.utils.DictionaryInfoUtils; -import com.android.inputmethod.latin.utils.LocaleUtils; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -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() - + "/Download"; - - private static String[] findDictionariesInTheDownloadedFolder() { - final File[] files = new File(SOURCE_FOLDER).listFiles(); - final ArrayList<String> eligibleList = new ArrayList<>(); - for (File f : files) { - final DictionaryHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(f); - if (null == header) continue; - eligibleList.add(f.getName()); - } - return eligibleList.toArray(new String[0]); - } - - public static void chooseAndInstallDictionary(final Context context) { - final String[] fileNames = findDictionariesInTheDownloadedFolder(); - if (0 == fileNames.length) { - showNoFileDialog(context); - } else if (1 == fileNames.length) { - askInstallFile(context, SOURCE_FOLDER, fileNames[0], null /* completeRunnable */); - } else { - showChooseFileDialog(context, fileNames); - } - } - - private static void showNoFileDialog(final Context context) { - new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) - .setMessage(R.string.read_external_dictionary_no_files_message) - .setPositiveButton(android.R.string.ok, new OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - dialog.dismiss(); - } - }).create().show(); - } - - private static void showChooseFileDialog(final Context context, final String[] fileNames) { - new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) - .setTitle(R.string.read_external_dictionary_multiple_files_title) - .setItems(fileNames, new OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - askInstallFile(context, SOURCE_FOLDER, fileNames[which], - null /* completeRunnable */); - } - }) - .create().show(); - } - - /** - * Shows a dialog which offers the user to install the external dictionary. - */ - public static void askInstallFile(final Context context, final String dirPath, - final String fileName, final Runnable completeRunnable) { - final File file = new File(dirPath, fileName.toString()); - final DictionaryHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file); - final StringBuilder message = new StringBuilder(); - final String locale = header.getLocaleString(); - for (String key : header.mDictionaryOptions.mAttributes.keySet()) { - message.append(key + " = " + header.mDictionaryOptions.mAttributes.get(key)); - message.append("\n"); - } - final String languageName = LocaleUtils.constructLocaleFromString(locale) - .getDisplayName(Locale.getDefault()); - final String title = String.format( - context.getString(R.string.read_external_dictionary_confirm_install_message), - languageName); - new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) - .setTitle(title) - .setMessage(message) - .setNegativeButton(android.R.string.cancel, new OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - dialog.dismiss(); - if (completeRunnable != null) { - completeRunnable.run(); - } - } - }).setPositiveButton(android.R.string.ok, new OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - installFile(context, file, header); - dialog.dismiss(); - if (completeRunnable != null) { - completeRunnable.run(); - } - } - }).setOnCancelListener(new OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - // Canceled by the user by hitting the back key - if (completeRunnable != null) { - completeRunnable.run(); - } - } - }).create().show(); - } - - private static void installFile(final Context context, final File file, - final DictionaryHeader header) { - BufferedOutputStream outputStream = null; - File tempFile = null; - try { - final String locale = header.getLocaleString(); - // Create the id for a main dictionary for this locale - final String id = BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY - + BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale; - final String finalFileName = DictionaryInfoUtils.getCacheFileName(id, locale, context); - final String tempFileName = BinaryDictionaryGetter.getTempFileName(id, context); - tempFile = new File(tempFileName); - tempFile.delete(); - outputStream = new BufferedOutputStream(new FileOutputStream(tempFile)); - final BufferedInputStream bufferedStream = new BufferedInputStream( - new FileInputStream(file)); - BinaryDictionaryFileDumper.checkMagicAndCopyFileTo(bufferedStream, outputStream); - outputStream.flush(); - final File finalFile = new File(finalFileName); - finalFile.delete(); - if (!tempFile.renameTo(finalFile)) { - throw new IOException("Can't move the file to its final name"); - } - } catch (IOException e) { - // There was an error: show a dialog - new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) - .setTitle(R.string.read_external_dictionary_error) - .setMessage(e.toString()) - .setPositiveButton(android.R.string.ok, new OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - dialog.dismiss(); - } - }).create().show(); - return; - } finally { - try { - if (null != outputStream) outputStream.close(); - if (null != tempFile) tempFile.delete(); - } catch (IOException e) { - // Don't do anything - } - } - } -} diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index fdab7f25f..5b3b28d75 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -17,7 +17,6 @@ package com.android.inputmethod.latin.inputlogic; import android.graphics.Color; -import android.inputmethodservice.InputMethodService; import android.os.SystemClock; import android.text.SpannableString; import android.text.Spanned; @@ -28,32 +27,28 @@ import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.inputmethod.CorrectionInfo; -import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; -import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; import com.android.inputmethod.compat.SuggestionSpanUtils; import com.android.inputmethod.event.Event; import com.android.inputmethod.event.InputTransaction; +import com.android.inputmethod.keyboard.Keyboard; 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 +56,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 +72,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,18 +82,10 @@ 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; - private final TextDecorator mTextDecorator = new TextDecorator(new TextDecorator.Listener() { - @Override - public void onClickComposingTextToAddToDictionary(final String word) { - mLatinIME.addWordToUserDictionary(word); - mLatinIME.dismissAddToDictionaryHint(); - } - }); - public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; // This has package visibility so it can be accessed from InputLogicHandler. /* package */ final WordComposer mWordComposer; @@ -144,13 +133,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,13 +157,9 @@ 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 */); } } @@ -204,6 +196,8 @@ public final class InputLogic { public void finishInput() { if (mWordComposer.isComposingWord()) { mConnection.finishComposingText(); + StatsUtils.onWordCommitUserTyped( + mWordComposer.getTypedWord(), mWordComposer.isBatchMode()); } resetComposingState(true /* alsoResetLastComposedWord */); mInputLogicHandler.reset(); @@ -231,9 +225,7 @@ public final class InputLogic { * @return the complete transaction object */ public InputTransaction onTextInput(final SettingsValues settingsValues, final Event event, - final int keyboardShiftMode, - // TODO: remove this argument - final LatinIME.UIHandler handler) { + final int keyboardShiftMode, final LatinIME.UIHandler handler) { final String rawText = event.getTextToCommit().toString(); final InputTransaction inputTransaction = new InputTransaction(settingsValues, event, SystemClock.uptimeMillis(), mSpaceState, @@ -247,9 +239,10 @@ public final class InputLogic { handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_TYPING); final String text = performSpecificTldProcessingOnTextInput(rawText); if (SpaceState.PHANTOM == mSpaceState) { - promotePhantomSpace(settingsValues); + insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); } mConnection.commitText(text, 1); + StatsUtils.onWordCommitUserTyped(mEnteredText, mWordComposer.isBatchMode()); mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState mSpaceState = SpaceState.NONE; @@ -260,20 +253,6 @@ public final class InputLogic { } /** - * Determines whether "Touch again to save" should be shown or not. - * @param suggestionInfo the suggested word chosen by the user. - * @return {@code true} if we should show the "Touch again to save" hint. - */ - private boolean shouldShowAddToDictionaryHint(final SuggestedWordInfo suggestionInfo) { - // We should show the "Touch again to save" hint if the user pressed the first entry - // AND it's in none of our current dictionaries (main, user or otherwise). - return (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_TYPED) - || suggestionInfo.isKindOf(SuggestedWordInfo.KIND_OOV_CORRECTION)) - && !mDictionaryFacilitator.isValidWord(suggestionInfo.mWord, true /* ignoreCase */) - && mDictionaryFacilitator.isUserDictionaryEnabled(); - } - - /** * A suggestion was picked from the suggestion strip. * @param settingsValues the current values of the settings. * @param suggestionInfo the suggestion info. @@ -285,12 +264,14 @@ public final class InputLogic { // interface public InputTransaction onPickSuggestionManually(final SettingsValues settingsValues, final SuggestedWordInfo suggestionInfo, final int keyboardShiftState, - // TODO: remove these arguments final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { final SuggestedWords suggestedWords = mSuggestedWords; final String suggestion = suggestionInfo.mWord; // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput if (suggestion.length() == 1 && suggestedWords.isPunctuationSuggestions()) { + // We still want to log a suggestion click. + StatsUtils.onPickSuggestionManually( + mSuggestedWords, suggestionInfo, mDictionaryFacilitator); // Word separators are suggested before the user inputs something. // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. final Event event = Event.createPunctuationSuggestionPickedEvent(suggestionInfo); @@ -312,7 +293,7 @@ public final class InputLogic { final int firstChar = Character.codePointAt(suggestion, 0); if (!settingsValues.isWordSeparator(firstChar) || settingsValues.isUsuallyPrecededBySpace(firstChar)) { - promotePhantomSpace(settingsValues); + insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); } } @@ -321,7 +302,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 */); @@ -330,7 +311,6 @@ public final class InputLogic { return inputTransaction; } - final boolean shouldShowAddToDictionaryHint = shouldShowAddToDictionaryHint(suggestionInfo); commitChosenWord(settingsValues, suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); mConnection.endBatchEdit(); @@ -340,13 +320,14 @@ public final class InputLogic { mSpaceState = SpaceState.PHANTOM; inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); - if (shouldShowAddToDictionaryHint) { - mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion); - } 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); - } + // 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, mDictionaryFacilitator); + StatsUtils.onWordCommitSuggestionPickedManually( + suggestionInfo.mWord, mWordComposer.isBatchMode()); return inputTransaction; } @@ -416,14 +397,8 @@ public final class InputLogic { // The cursor has been moved : we now accept to perform recapitalization mRecapitalizeStatus.enable(); - // We moved the cursor and need to invalidate the indicator right now. - mTextDecorator.reset(); - // Remaining background color that was used for the add-to-dictionary indicator should be - // 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,9 +417,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, - // TODO: remove these arguments + public InputTransaction onCodeInput(final SettingsValues settingsValues, + @Nonnull final Event event, final int keyboardShiftMode, final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { final Event processedEvent = mWordComposer.processEvent(event); final InputTransaction inputTransaction = new InputTransaction(settingsValues, @@ -491,17 +465,14 @@ public final class InputLogic { } public void onStartBatchInput(final SettingsValues settingsValues, - // TODO: remove these arguments final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { mInputLogicHandler.onStartBatchInput(); handler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); + SuggestedWords.getEmptyInstance(), false /* dismissGestureFloatingPreviewText */); handler.cancelUpdateSuggestionStrip(); ++mAutoCommitSequenceNumber; mConnection.beginBatchEdit(); - if (!mWordComposer.isComposingWord()) { - mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); - } else { + if (mWordComposer.isComposingWord()) { if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { // If we are in the middle of a recorrection, we need to commit the recorrection // first so that we can insert the batch input at the current cursor position. @@ -557,30 +528,7 @@ public final class InputLogic { * earlier sequence number. */ private int mAutoCommitSequenceNumber = 1; - public void onUpdateBatchInput(final SettingsValues settingsValues, - final InputPointers batchPointers, - // TODO: remove these arguments - final KeyboardSwitcher keyboardSwitcher) { - if (settingsValues.mPhraseGestureEnabled) { - final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate(); - // If these suggested words have been generated with out of date input pointers, then - // we skip auto-commit (see comments above on the mSequenceNumber member). - if (null != candidate - && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) { - if (candidate.mSourceDict.shouldAutoCommit(candidate)) { - final String[] commitParts = candidate.mWord.split(Constants.WORD_SEPARATOR, 2); - batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); - promotePhantomSpace(settingsValues); - mConnection.commitText(commitParts[0], 0); - mSpaceState = SpaceState.PHANTOM; - keyboardSwitcher.requestUpdatingShiftState( - getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState()); - mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode( - settingsValues, keyboardSwitcher.getKeyboardShiftMode())); - ++mAutoCommitSequenceNumber; - } - } - } + public void onUpdateBatchInput(final InputPointers batchPointers) { mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber); } @@ -589,27 +537,25 @@ public final class InputLogic { ++mAutoCommitSequenceNumber; } - // TODO: remove this argument 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 = suggestedWords.mTypedWordInfo; } - mWordComposer.setAutoCorrection(autoCorrection); + mWordComposer.setAutoCorrection(suggestedWordInfo); } mSuggestedWords = suggestedWords; final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; @@ -666,7 +612,6 @@ public final class InputLogic { * @param inputTransaction The transaction in progress. */ private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction, - // TODO: remove these arguments final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { switch (event.mKeyCode) { case Constants.CODE_DELETE: @@ -683,7 +628,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,14 +656,13 @@ 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 final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER, event.mKeyCode, event.mX, event.mY, event.isKeyRepeat()); handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler); @@ -742,7 +686,6 @@ public final class InputLogic { */ private void handleNonFunctionalEvent(final Event event, final InputTransaction inputTransaction, - // TODO: remove this argument final LatinIME.UIHandler handler) { inputTransaction.setDidAffectContents(); switch (event.mCodePoint) { @@ -788,18 +731,7 @@ public final class InputLogic { */ private void handleNonSpecialCharacterEvent(final Event event, final InputTransaction inputTransaction, - // TODO: remove this argument final LatinIME.UIHandler handler) { - if (!mWordComposer.isComposingWord()) { - mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); - // In case the "add to dictionary" hint was still displayed. - // TODO: Do we really need to check if we have composing text here? - if (mSuggestionStripViewAccessor.isShowingAddToDictionaryHint()) { - mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); - mTextDecorator.reset(); - } - } - final int codePoint = event.mCodePoint; mSpaceState = SpaceState.NONE; if (inputTransaction.mSettingsValues.isWordSeparator(codePoint) @@ -843,7 +775,7 @@ public final class InputLogic { // Sanity check throw new RuntimeException("Should not be composing here"); } - promotePhantomSpace(settingsValues); + insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); } if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { @@ -904,7 +836,6 @@ public final class InputLogic { * @param inputTransaction The transaction in progress. */ private void handleSeparatorEvent(final Event event, final InputTransaction inputTransaction, - // TODO: remove this argument final LatinIME.UIHandler handler) { final int codePoint = event.mCodePoint; final SettingsValues settingsValues = inputTransaction.mSettingsValues; @@ -954,12 +885,13 @@ public final class InputLogic { } if (needsPrecedingSpace) { - promotePhantomSpace(settingsValues); + insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); } if (tryPerformDoubleSpacePeriod(event, inputTransaction)) { mSpaceState = SpaceState.DOUBLE; inputTransaction.setRequiresUpdateSuggestions(); + StatsUtils.onDoubleSpacePeriod(); } else if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) { mSpaceState = SpaceState.SWAP_PUNCTUATION; mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); @@ -1011,7 +943,6 @@ public final class InputLogic { * @param inputTransaction The transaction in progress. */ private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction, - // TODO: remove this argument, put it into settingsValues final int currentKeyboardScriptId) { mSpaceState = SpaceState.NONE; mDeleteCount++; @@ -1041,10 +972,13 @@ public final class InputLogic { mWordComposer.reset(); mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); if (!TextUtils.isEmpty(rejectedSuggestion)) { - mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion); + unlearnWord(rejectedSuggestion, inputTransaction.mSettingsValues, + Constants.EVENT_REJECTION); } + StatsUtils.onBackspaceWordDelete(rejectedSuggestion.length()); } else { mWordComposer.applyProcessedEvent(event); + StatsUtils.onBackspacePressed(1); } if (mWordComposer.isComposingWord()) { setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); @@ -1054,7 +988,24 @@ 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()); + // Restart suggestions when backspacing into a reverted word. This is required for + // the final corrected word to be learned, as learning only occurs when suggestions + // are active. + // + // Note: restartSuggestionsOnWordTouchedByCursor is already called for normal + // (non-revert) backspace handling. + if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() + && inputTransaction.mSettingsValues.mSpacingAndPunctuations + .mCurrentLanguageHasSpaces + && !mConnection.isCursorFollowedByWordCharacter( + inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { + restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues, + false /* forStartInput */, currentKeyboardScriptId); + } return; } if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { @@ -1062,6 +1013,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,21 +1022,26 @@ 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; } } + boolean hasUnlearnedWordBeingDeleted = false; + // No cancelling of commit/double space/swap: we have a regular backspace. // We should backspace one char and restart suggestion if at the end of a word. if (mConnection.hasSelection()) { @@ -1094,25 +1051,36 @@ 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) { + // If this is an accelerated (i.e., double) deletion, then we need to + // consider unlearning here because we may have already reached + // the previous word, and will lose it after next deletion. + hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( + inputTransaction.mSettingsValues, currentKeyboardScriptId); sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + totalDeletedLength++; } + StatsUtils.onBackspacePressed(totalDeletedLength); } else { final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); if (codePointBeforeCursor == Constants.NOT_A_CODE) { @@ -1123,32 +1091,81 @@ 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) { + // If this is an accelerated (i.e., double) deletion, then we need to + // consider unlearning here because we may have already reached + // the previous word, and will lose it after next deletion. + hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( + inputTransaction.mSettingsValues, currentKeyboardScriptId); final int codePointBeforeCursorToDeleteAgain = mConnection.getCodePointBeforeCursor(); if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( codePointBeforeCursorToDeleteAgain) ? 2 : 1; mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); + totalDeletedLength += lengthToDeleteAgain; } } + StatsUtils.onBackspacePressed(totalDeletedLength); } } - if (inputTransaction.mSettingsValues - .isSuggestionsEnabledPerUserSettings() + if (!hasUnlearnedWordBeingDeleted) { + // Consider unlearning the word being deleted (if we have not done so already). + unlearnWordBeingDeleted( + inputTransaction.mSettingsValues, currentKeyboardScriptId); + } + if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() && inputTransaction.mSettingsValues.mSpacingAndPunctuations .mCurrentLanguageHasSpaces && !mConnection.isCursorFollowedByWordCharacter( inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues, - true /* shouldIncludeResumedWordInSuggestions */, currentKeyboardScriptId); + false /* forStartInput */, currentKeyboardScriptId); + } + } + } + + boolean unlearnWordBeingDeleted( + final SettingsValues settingsValues,final int currentKeyboardScriptId) { + // If we just started backspacing to delete a previous word (but have not + // entered the composing state yet), unlearn the word. + // TODO: Consider tracking whether or not this word was typed by the user. + if (!mConnection.hasSelection() + && settingsValues.isSuggestionsEnabledPerUserSettings() + && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces + && !mConnection.isCursorFollowedByWordCharacter( + settingsValues.mSpacingAndPunctuations)) { + final TextRange range = mConnection.getWordRangeAtCursor( + settingsValues.mSpacingAndPunctuations, + currentKeyboardScriptId); + if (range == null) { + // Happens if we don't have an input connection at all. + return false; + } + final String wordBeingDeleted = range.mWord.toString(); + if (!wordBeingDeleted.isEmpty()) { + unlearnWord(wordBeingDeleted, settingsValues, + Constants.EVENT_BACKSPACE); + return true; } } + return false; + } + + void unlearnWord(final String word, final SettingsValues settingsValues, final int eventType) { + final NgramContext ngramContext = mConnection.getNgramContextFromNthPreviousWord( + settingsValues.mSpacingAndPunctuations, 2); + final long timeStampInSeconds = TimeUnit.MILLISECONDS.toSeconds( + System.currentTimeMillis()); + mDictionaryFacilitator.unlearnFromUserHistory( + word, ngramContext, timeStampInSeconds, eventType); } /** @@ -1253,7 +1270,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 +1355,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 +1367,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 +1379,7 @@ public final class InputLogic { + "requested!"); } // Clear the suggestions strip. - mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.EMPTY); + mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.getEmptyInstance()); return; } @@ -1374,14 +1393,20 @@ public final class InputLogic { new OnGetSuggestedWordsCallback() { @Override public void onGetSuggestedWords(final SuggestedWords suggestedWords) { - final String typedWord = mWordComposer.getTypedWord(); + final String typedWordString = mWordComposer.getTypedWord(); + final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo( + typedWordString, "" /* prevWordsContext */, + SuggestedWordInfo.MAX_SCORE, + SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE); // Show new suggestions if we have at least one. Otherwise keep the old // suggestions with the new typed word. Exception: if the length of the // typed word is <= 1 (after a deletion typically) we clear old suggestions. - if (suggestedWords.size() > 1 || typedWord.length() <= 1) { + if (suggestedWords.size() > 1 || typedWordString.length() <= 1) { holder.set(suggestedWords); } else { - holder.set(retrieveOlderSuggestions(typedWord, mSuggestedWords)); + holder.set(retrieveOlderSuggestions(typedWordInfo, mSuggestedWords)); } } } @@ -1400,12 +1425,12 @@ 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. + * @param forStartInput whether we're doing this in answer to starting the input (as opposed + * to a cursor move, for example). In ICS, there is a platform bug that we need to work + * around only when we come here at input start time. */ - // TODO: make this private. public void restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues, - final boolean shouldIncludeResumedWordInSuggestions, + final boolean forStartInput, // 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 @@ -1452,15 +1477,14 @@ public final class InputLogic { final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); 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 */)); - } - if (!isResumableWord(settingsValues, typedWord)) { + final String typedWordString = range.mWord.toString(); + final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo(typedWordString, + "" /* prevWordsContext */, SuggestedWords.MAX_SUGGESTIONS + 1, + SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); + suggestions.add(typedWordInfo); + if (!isResumableWord(settingsValues, typedWordString)) { mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); return; } @@ -1468,9 +1492,9 @@ public final class InputLogic { for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) { for (final String s : span.getSuggestions()) { ++i; - if (!TextUtils.equals(s, typedWord)) { + if (!TextUtils.equals(s, typedWordString)) { suggestions.add(new SuggestedWordInfo(s, - SuggestedWords.MAX_SUGGESTIONS - i, + "" /* prevWordsContext */, SuggestedWords.MAX_SUGGESTIONS - i, SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, SuggestedWordInfo.NOT_A_CONFIDENCE @@ -1478,47 +1502,25 @@ 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); + final int[] codePoints = StringUtils.toCodePointArray(typedWordString); mWordComposer.setComposingWord(codePoints, mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); mWordComposer.setCursorPositionWithinWord( - typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor)); - mConnection.maybeMoveTheCursorAroundAndRestoreToWorkaroundABug(); + typedWordString.codePointCount(0, numberOfCharsInWordBeforeCursor)); + if (forStartInput) { + 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 @@ -1526,14 +1528,18 @@ public final class InputLogic { // color of the word in the suggestion strip changes according to this parameter, // and false gives the correct color. final SuggestedWords suggestedWords = new SuggestedWords(suggestions, - null /* rawSuggestions */, typedWord, false /* typedWordValid */, + null /* rawSuggestions */, typedWordInfo, 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 +1557,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? @@ -1569,9 +1579,11 @@ public final class InputLogic { } mConnection.deleteSurroundingText(deleteLength, 0); if (!TextUtils.isEmpty(committedWord)) { - mDictionaryFacilitator.removeWordFromPersonalizedDicts(committedWordString); + unlearnWord(committedWordString, inputTransaction.mSettingsValues, + Constants.EVENT_REVERT); } - 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; @@ -1583,15 +1595,11 @@ public final class InputLogic { // First, add the committed word to the list of suggestions. suggestions.add(committedWordString); for (final Object span : spans) { - // If this is a suggestion span, we check that the locale is the right one, and - // that the word is not the committed word. That should mostly be the case. + // If this is a suggestion span, we check that the word is not the committed word. + // That should mostly be the case. // Given this, we add it to the list of suggestions, otherwise we discard it. if (span instanceof SuggestionSpan) { final SuggestionSpan suggestionSpan = (SuggestionSpan)span; - if (!suggestionSpan.getLocale().equals( - inputTransaction.mSettingsValues.mLocale.toString())) { - continue; - } for (final String suggestion : suggestionSpan.getSuggestions()) { if (!suggestion.equals(committedWordString)) { suggestions.add(suggestion); @@ -1604,24 +1612,17 @@ 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 */); } - final boolean shouldShowAddToDictionaryForTypedWord = - shouldShowAddToDictionaryForTypedWord(mLastComposedWord, settingsValues); - if (inputTransaction.mSettingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) { - // For languages with spaces, we revert to the typed string, but the cursor is still - // after the separator so we don't resume suggestions. If the user wants to correct - // the word, they have to press backspace again. - if (shouldShowAddToDictionaryForTypedWord) { - mConnection.commitTextWithBackgroundColor(textToCommit, 1, - settingsValues.mTextHighlightColorForAddToDictionaryIndicator, - originallyTypedWordString.length()); - } else { - mConnection.commitText(textToCommit, 1); + mConnection.commitText(textToCommit, 1); + if (usePhantomSpace) { + mSpaceState = SpaceState.PHANTOM; } } else { // For languages without spaces, we revert the typed string but the cursor is flush @@ -1629,32 +1630,13 @@ public final class InputLogic { final int[] codePoints = StringUtils.toCodePointArray(stringToCommit); mWordComposer.setComposingWord(codePoints, mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); - if (shouldShowAddToDictionaryForTypedWord) { - setComposingTextInternalWithBackgroundColor(textToCommit, 1, - settingsValues.mTextHighlightColorForAddToDictionaryIndicator, - originallyTypedWordString.length()); - } else { - setComposingTextInternal(textToCommit, 1); - } + setComposingTextInternal(textToCommit, 1); } // Don't restart suggestion yet. We'll restart if the user deletes the separator. mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - if (shouldShowAddToDictionaryForTypedWord) { - // Due to the API limitation as of L, we cannot reliably retrieve the reverted text - // when the separator causes line breaking. Until this API limitation is addressed in - // the framework, show the indicator only when the separator doesn't contain - // line-breaking characters. - if (!StringUtils.hasLineBreakCharacter(separatorString)) { - mTextDecorator.showAddToDictionaryIndicator(originallyTypedWordString, - mConnection.getExpectedSelectionStart(), - mConnection.getExpectedSelectionEnd()); - } - mSuggestionStripViewAccessor.showAddToDictionaryHint(originallyTypedWordString); - } else { - // We have a separator between the word and the cursor: we should show predictions. - inputTransaction.setRequiresUpdateSuggestions(); - } + // We have a separator between the word and the cursor: we should show predictions. + inputTransaction.setRequiresUpdateSuggestions(); } /** @@ -1720,26 +1702,25 @@ 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 +1773,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; } /** @@ -1845,21 +1825,21 @@ public final class InputLogic { * Make a {@link com.android.inputmethod.latin.SuggestedWords} object containing a typed word * and obsolete suggestions. * See {@link com.android.inputmethod.latin.SuggestedWords#getTypedWordAndPreviousSuggestions( - * String, com.android.inputmethod.latin.SuggestedWords)}. - * @param typedWord The typed word as a string. + * SuggestedWordInfo, com.android.inputmethod.latin.SuggestedWords)}. + * @param typedWordInfo The typed word as a SuggestedWordInfo. * @param previousSuggestedWords The previously suggested words. * @return Obsolete suggestions with the newly typed word. */ - private SuggestedWords retrieveOlderSuggestions(final String typedWord, + static SuggestedWords retrieveOlderSuggestions(final SuggestedWordInfo typedWordInfo, 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); + SuggestedWords.getTypedWordAndPreviousSuggestions(typedWordInfo, oldSuggestedWords); return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */, - false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, - true /* isObsoleteSuggestions */, oldSuggestedWords.mInputStyle); + typedWordInfo, false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, + true /* isObsoleteSuggestions */, oldSuggestedWords.mInputStyle, + SuggestedWords.NOT_A_SEQUENCE_NUMBER); } /** @@ -1936,14 +1916,14 @@ public final class InputLogic { } /** - * Promote a phantom space to an actual space. + * Insert an automatic space, if the options allow it. * - * This essentially inserts a space, and that's it. It just checks the options and the text - * before the cursor are appropriate before doing it. + * This checks the options and the text before the cursor are appropriate before inserting + * an automatic space. * * @param settingsValues the current values of the settings. */ - private void promotePhantomSpace(final SettingsValues settingsValues) { + private void insertAutomaticSpaceIfOptionsAndTextAllow(final SettingsValues settingsValues) { if (settingsValues.shouldInsertSpacesAutomatically() && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces && !mConnection.textBeforeCursorLooksLikeURL()) { @@ -1957,36 +1937,17 @@ public final class InputLogic { * @param suggestedWords suggestedWords to use. */ public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, - final SuggestedWords suggestedWords, - // TODO: remove this argument - final KeyboardSwitcher keyboardSwitcher) { + final SuggestedWords suggestedWords, final KeyboardSwitcher keyboardSwitcher) { final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0); if (TextUtils.isEmpty(batchInputText)) { return; } mConnection.beginBatchEdit(); if (SpaceState.PHANTOM == mSpaceState) { - promotePhantomSpace(settingsValues); - } - final SuggestedWordInfo autoCommitCandidate = mSuggestedWords.getAutoCommitCandidate(); - // Commit except the last word for phrase gesture if the top suggestion is eligible for auto - // commit. - if (settingsValues.mPhraseGestureEnabled && null != autoCommitCandidate) { - // Find the last space - final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; - if (0 != indexOfLastSpace) { - mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1); - final SuggestedWords suggestedWordsForLastWordOfPhraseGesture = - suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture(); - mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture); - } - final String lastWord = batchInputText.substring(indexOfLastSpace); - mWordComposer.setBatchInputWord(lastWord); - setComposingTextInternal(lastWord, 1); - } else { - mWordComposer.setBatchInputWord(batchInputText); - setComposingTextInternal(batchInputText, 1); + insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); } + mWordComposer.setBatchInputWord(batchInputText); + setComposingTextInternal(batchInputText, 1); mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState mSpaceState = SpaceState.PHANTOM; @@ -2009,13 +1970,14 @@ public final class InputLogic { * @param settingsValues the current values of the settings. * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none. */ - // TODO: Make this private public void commitTyped(final SettingsValues settingsValues, final String separatorString) { 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); } } @@ -2036,9 +1998,7 @@ public final class InputLogic { * @param separator the separator that's causing the commit to happen. */ private void commitCurrentAutoCorrection(final SettingsValues settingsValues, - final String separator, - // TODO: Remove this argument. - final LatinIME.UIHandler handler) { + final String separator, final LatinIME.UIHandler handler) { // Complete any pending suggestions query first if (handler.hasPendingUpdateSuggestions()) { handler.cancelUpdateSuggestionStrip(); @@ -2052,18 +2012,19 @@ 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 (!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 +2032,16 @@ 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)); + String prevWordsContext = (autoCorrectionOrNull != null) + ? autoCorrectionOrNull.mPrevWordsContext + : ""; + StatsUtils.onAutoCorrection(typedWord, stringToCommit, isBatchMode, + mDictionaryFacilitator, prevWordsContext); + StatsUtils.onWordCommitAutoCorrect(stringToCommit, isBatchMode); + } else { + StatsUtils.onWordCommitUserTyped(stringToCommit, isBatchMode); } } } @@ -2091,20 +2060,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); } /** @@ -2118,11 +2087,8 @@ public final class InputLogic { * @param remainingTries How many times we may try again before giving up. * @return whether true if the caches were successfully reset, false otherwise. */ - // TODO: make this private public boolean retryResetCachesAndReturnSuccess(final boolean tryResumeSuggestions, - final int remainingTries, - // TODO: remove these arguments - final LatinIME.UIHandler handler) { + final int remainingTries, final LatinIME.UIHandler handler) { final boolean shouldFinishComposition = mConnection.hasSelection() || !mConnection.isCursorPositionKnown(); if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess( @@ -2137,30 +2103,25 @@ 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; } public void getSuggestedWords(final SettingsValues settingsValues, - final ProximityInfo proximityInfo, final int keyboardShiftMode, final int inputStyle, + final Keyboard keyboard, final int keyboardShiftMode, final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { 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, // hence 2; if we aren't, we should just skip whitespace if any, so 1. mWordComposer.isComposingWord() ? 2 : 1), - proximityInfo, - new SettingsValuesForSuggestion(settingsValues.mBlockPotentiallyOffensive, - settingsValues.mPhraseGestureEnabled, - settingsValues.mAdditionalFeaturesSettingValues), + keyboard, + new SettingsValuesForSuggestion(settingsValues.mBlockPotentiallyOffensive), settingsValues.mAutoCorrectionEnabledPerUserSettings, inputStyle, sequenceNumber, callback); } @@ -2171,7 +2132,7 @@ public final class InputLogic { * * <p>Currently using this method is optional and you can still directly call * {@link RichInputConnection#setComposingText(CharSequence, int)}, but it is recommended to - * use this method whenever possible to optimize the behavior of {@link TextDecorator}.<p> + * use this method whenever possible.<p> * <p>TODO: Should we move this mechanism to {@link RichInputConnection}?</p> * * @param newComposingText the composing text to be set @@ -2216,70 +2177,44 @@ public final class InputLogic { mConnection.setComposingText(composingTextToBeSet, newCursorPosition); } - ////////////////////////////////////////////////////////////////////////////////////////////// - // Following methods are tentatively placed in this class for the integration with - // TextDecorator. - // TODO: Decouple things that are not related to the input logic. - ////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * 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) { - mTextDecorator.setUiOperator(uiOperator); - } - /** - * Must be called from {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is - * called. - * @param info The wrapper object with which we can access cursor/anchor info. + * 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 void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) { - mTextDecorator.onUpdateCursorAnchorInfo(info); + public PrivateCommandPerformer getPrivateCommandPerformer() { + return mConnection; } /** - * Must be called when {@link InputMethodService#updateFullscreenMode} is called. - * @param isFullscreen {@code true} if the input method is in full-screen mode. - */ - public void onUpdateFullscreenMode(final boolean isFullscreen) { - mTextDecorator.notifyFullScreenMode(isFullscreen); - } - - /** - * Must be called from {@link LatinIME#addWordToUserDictionary(String)}. + * 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. */ - public void onAddWordToUserDictionary() { - mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); - mTextDecorator.reset(); + // 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(); } /** - * Returns whether the add to dictionary indicator should be shown or not. - * @param lastComposedWord the last composed word information. - * @param settingsValues the current settings value. - * @return {@code true} if the commit indicator should be shown. + * 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. */ - private boolean shouldShowAddToDictionaryForTypedWord(final LastComposedWord lastComposedWord, - final SettingsValues settingsValues) { - if (!mConnection.isCursorAnchorInfoMonitorEnabled()) { - // We cannot help in this case because we are heavily relying on this new API. - return false; - } - if (!settingsValues.mShouldShowUiToAcceptTypedWord) { - return false; - } - if (TextUtils.isEmpty(lastComposedWord.mTypedWord)) { - return false; - } - if (TextUtils.equals(lastComposedWord.mTypedWord, lastComposedWord.mCommittedWord)) { - return false; - } - if (!mDictionaryFacilitator.isUserDictionaryEnabled()) { - return false; - } - return !mDictionaryFacilitator.isValidWord(lastComposedWord.mTypedWord, - true /* ignoreCase */); + // 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(); } } 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..4d253b0cb 100644 --- a/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java +++ b/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java @@ -19,13 +19,24 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions; import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * Class representing dictionary header. */ public final class DictionaryHeader { public final int mBodyOffset; + @Nonnull public final DictionaryOptions mDictionaryOptions; + @Nonnull public final FormatOptions mFormatOptions; + @Nonnull + public final String mLocaleString; + @Nonnull + public final String mVersionString; + @Nonnull + public final String mIdString; // Note that these are corresponding definitions in native code in latinime::HeaderPolicy // and latinime::HeaderReadWriteUtils. @@ -38,49 +49,40 @@ 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 { + public DictionaryHeader(final int headerSize, + @Nonnull final DictionaryOptions dictionaryOptions, + @Nonnull final FormatOptions formatOptions) throws UnsupportedFormatException { mDictionaryOptions = dictionaryOptions; mFormatOptions = formatOptions; mBodyOffset = formatOptions.mVersion < FormatSpec.VERSION4 ? headerSize : 0; - if (null == getLocaleString()) { + final String localeString = dictionaryOptions.mAttributes.get(DICTIONARY_LOCALE_KEY); + if (null == localeString) { throw new UnsupportedFormatException("Cannot create a FileHeader without a locale"); } - if (null == getVersion()) { + final String versionString = dictionaryOptions.mAttributes.get(DICTIONARY_VERSION_KEY); + if (null == versionString) { throw new UnsupportedFormatException( "Cannot create a FileHeader without a version"); } - if (null == getId()) { + final String idString = dictionaryOptions.mAttributes.get(DICTIONARY_ID_KEY); + if (null == idString) { throw new UnsupportedFormatException("Cannot create a FileHeader without an ID"); } - } - - // Helper method to get the locale as a String - public String getLocaleString() { - return mDictionaryOptions.mAttributes.get(DICTIONARY_LOCALE_KEY); - } - - // Helper method to get the version String - public String getVersion() { - return mDictionaryOptions.mAttributes.get(DICTIONARY_VERSION_KEY); - } - - // Helper method to get the dictionary ID as a String - public String getId() { - return mDictionaryOptions.mAttributes.get(DICTIONARY_ID_KEY); + mLocaleString = localeString; + mVersionString = versionString; + mIdString = idString; } // Helper method to get the description + @Nullable public String getDescription() { // TODO: Right now each dictionary file comes with a description in its own language. // It will display as is no matter the device's locale. It should be internationalized. diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java index a2ae74b20..e422c4cd2 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.define.DecoderSpecificConstants; 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,31 +161,30 @@ 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; - // 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 VERSION201 = 201; + public static final int VERSION202 = 202; + // format version for Fava Dictionaries. + public static final int VERSION_DELIGHT3 = 86736212; + public static final int VERSION402 = 402; + public static final int VERSION403 = 403; + public static final int VERSION4 = VERSION403; + public static final int MINIMUM_SUPPORTED_STATIC_VERSION = VERSION202; + public static final int MAXIMUM_SUPPORTED_STATIC_VERSION = VERSION_DELIGHT3; + static final int MINIMUM_SUPPORTED_DYNAMIC_VERSION = VERSION4; + static final int MAXIMUM_SUPPORTED_DYNAMIC_VERSION = VERSION403; // 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; + static final int MAX_WORD_LENGTH = DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH; // These flags are used only in the static dictionary. static final int MASK_CHILDREN_ADDRESS_TYPE = 0xC0; @@ -218,14 +199,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 +214,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 +236,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..264e75710 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. * @@ -32,31 +37,35 @@ import java.util.Arrays; 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 mHasShortcuts; - public final boolean mHasBigrams; + public final boolean mIsPossiblyOffensive; + 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(); - mHasShortcuts = shortcutTargets != null && !shortcutTargets.isEmpty(); + mIsPossiblyOffensive = isPossiblyOffensive; + mHasNgrams = bigrams != null && !bigrams.isEmpty(); } private static ProbabilityInfo createProbabilityInfoFromArray(final int[] probabilityInfo) { @@ -70,36 +79,54 @@ 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 isBeginningOfSentence, final int[] probabilityInfo, - final ArrayList<int[]> bigramTargets, final ArrayList<int[]> bigramProbabilityInfo, - final ArrayList<int[]> shortcutTargets, - final ArrayList<Integer> shortcutProbabilities) { + final ArrayList<int[][]> ngramPrevWordsArray, + final ArrayList<boolean[]> ngramPrevWordIsBeginningOfSentenceArray, + final ArrayList<int[]> ngramTargets, final ArrayList<int[]> ngramProbabilityInfo) { 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; - 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)))); + mIsPossiblyOffensive = isPossiblyOffensive; + 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++) { - final String shortcutTargetString = - StringUtils.getStringFromNullTerminatedCodePointArray(shortcutTargets.get(i)); - mShortcutTargets.add( - new WeightedString(shortcutTargetString, shortcutProbabilities.get(i))); + // 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() { @@ -110,10 +137,9 @@ public final class WordProperty implements Comparable<WordProperty> { return Arrays.hashCode(new Object[] { word.mWord, word.mProbabilityInfo, - word.mShortcutTargets.hashCode(), - word.mBigrams.hashCode(), + word.mNgrams, word.mIsNotAWord, - word.mIsBlacklistEntry + word.mIsPossiblyOffensive }); } @@ -141,10 +167,18 @@ public final class WordProperty implements Comparable<WordProperty> { if (o == this) return true; 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; + return mProbabilityInfo.equals(w.mProbabilityInfo) + && mWord.equals(w.mWord) && equals(mNgrams, w.mNgrams) + && mIsNotAWord == w.mIsNotAWord && mIsPossiblyOffensive == w.mIsPossiblyOffensive + && mHasNgrams == 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 +191,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 deleted file mode 100644 index ac55b9333..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/ContextualDictionary.java +++ /dev/null @@ -1,54 +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.personalization; - -import android.content.Context; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.ExpandableBinaryDictionary; - -import java.io.File; -import java.util.Locale; - -public class ContextualDictionary extends ExpandableBinaryDictionary { - /* package */ static final String NAME = ContextualDictionary.class.getSimpleName(); - - private ContextualDictionary(final Context context, final Locale locale, - final File dictFile) { - super(context, getDictName(NAME, locale, dictFile), locale, Dictionary.TYPE_CONTEXTUAL, - dictFile); - // Always reset the contents. - clear(); - } - - @UsedForTesting - public static ContextualDictionary getDictionary(final Context context, final Locale locale, - final File dictFile, final String dictNamePrefix) { - return new ContextualDictionary(context, locale, dictFile); - } - - @Override - public boolean isValidWord(final String word) { - // Strings out of this dictionary should not be considered existing words. - return false; - } - - @Override - protected void loadInitialContentsLocked() { - } -} diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java deleted file mode 100644 index 1ba7b366f..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2013 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.personalization; - -import android.content.Context; - -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.ExpandableBinaryDictionary; -import com.android.inputmethod.latin.makedict.DictionaryHeader; - -import java.io.File; -import java.util.Locale; -import java.util.Map; - -/** - * This class is a base class of a dictionary that supports decaying for the personalized language - * model. - */ -public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableBinaryDictionary { - private static final boolean DBG_DUMP_ON_CLOSE = false; - - /** Any pair being typed or picked */ - public static final int FREQUENCY_FOR_TYPED = 2; - - 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(); - } - } - - @Override - public void close() { - if (DBG_DUMP_ON_CLOSE) { - dumpAllWordsForDebug(); - } - // Flush pending writes. - asyncFlushBinaryDictionary(); - super.close(); - } - - @Override - protected Map<String, String> getHeaderAttributeMap() { - final Map<String, String> attributeMap = super.getHeaderAttributeMap(); - attributeMap.put(DictionaryHeader.USES_FORGETTING_CURVE_KEY, - DictionaryHeader.ATTRIBUTE_VALUE_TRUE); - attributeMap.put(DictionaryHeader.HAS_HISTORICAL_INFO_KEY, - DictionaryHeader.ATTRIBUTE_VALUE_TRUE); - return attributeMap; - } - - @Override - protected void loadInitialContentsLocked() { - // No initial contents. - } - - /* package */ void runGCIfRequired() { - runGCIfRequired(false /* mindsBlockByGC */); - } - - @Override - public boolean isValidWord(final String word) { - // Strings out of this dictionary should not be considered existing words. - return false; - } -} diff --git a/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java b/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java deleted file mode 100644 index 221bb9a8f..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2013 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.personalization; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import java.util.concurrent.TimeUnit; - -/** - * Broadcast receiver for periodically updating decaying dictionaries. - */ -public class DictionaryDecayBroadcastReciever extends BroadcastReceiver { - /** - * The root domain for the personalization. - */ - private static final String PERSONALIZATION_DOMAIN = - "com.android.inputmethod.latin.personalization"; - - /** - * The action of the intent to tell the time to decay dictionaries. - */ - private static final String DICTIONARY_DECAY_INTENT_ACTION = - PERSONALIZATION_DOMAIN + ".DICT_DECAY"; - - /** - * Interval to update for decaying dictionaries. - */ - /* package */ static final long DICTIONARY_DECAY_INTERVAL = TimeUnit.MINUTES.toMillis(60); - - public static void setUpIntervalAlarmForDictionaryDecaying(Context context) { - AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); - final Intent updateIntent = new Intent(DICTIONARY_DECAY_INTENT_ACTION); - updateIntent.setClass(context, DictionaryDecayBroadcastReciever.class); - final long alarmTime = System.currentTimeMillis() + DICTIONARY_DECAY_INTERVAL; - final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0 /* requestCode */, - updateIntent, PendingIntent.FLAG_CANCEL_CURRENT); - if (null != alarmManager) alarmManager.setInexactRepeating(AlarmManager.RTC, - alarmTime, DICTIONARY_DECAY_INTERVAL, pendingIntent); - } - - @Override - public void onReceive(final Context context, final Intent intent) { - final String action = intent.getAction(); - if (action.equals(DICTIONARY_DECAY_INTENT_ACTION)) { - PersonalizationHelper.runGCOnAllOpenedUserHistoryDictionaries(); - PersonalizationHelper.runGCOnAllOpenedPersonalizationDictionaries(); - } - } -} diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java deleted file mode 100644 index 9d72de8c5..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java +++ /dev/null @@ -1,37 +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.personalization; - -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -public class PersonalizationDataChunk { - public final boolean mInputByUser; - public final List<String> mTokens; - public final int mTimestampInSeconds; - public final String mPackageName; - public final Locale mlocale = null; - - public PersonalizationDataChunk(boolean inputByUser, final List<String> tokens, - final int timestampInSeconds, final String packageName) { - mInputByUser = inputByUser; - mTokens = Collections.unmodifiableList(tokens); - mTimestampInSeconds = timestampInSeconds; - mPackageName = packageName; - } -} diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java deleted file mode 100644 index f2ad22ac7..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2013 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.personalization; - -import android.content.Context; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Dictionary; - -import java.io.File; -import java.util.Locale; - -public class PersonalizationDictionary extends DecayingExpandableBinaryDictionaryBase { - /* package */ static final String NAME = PersonalizationDictionary.class.getSimpleName(); - - // TODO: Make this constructor private - /* package */ PersonalizationDictionary(final Context context, final Locale locale) { - super(context, getDictName(NAME, locale, null /* dictFile */), locale, - Dictionary.TYPE_PERSONALIZATION, null /* dictFile */); - } - - @UsedForTesting - 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/PersonalizationHelper.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java index 331f85e0e..298e46c0a 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java @@ -19,130 +19,76 @@ package com.android.inputmethod.latin.personalization; import android.content.Context; import android.util.Log; -import com.android.inputmethod.latin.utils.FileUtils; +import com.android.inputmethod.latin.common.FileUtils; import java.io.File; import java.io.FilenameFilter; import java.lang.ref.SoftReference; import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Helps handle and manage personalized dictionaries such as {@link UserHistoryDictionary}. + */ public class PersonalizationHelper { private static final String TAG = PersonalizationHelper.class.getSimpleName(); private static final boolean DEBUG = false; + private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>> sLangUserHistoryDictCache = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap<String, SoftReference<PersonalizationDictionary>> - sLangPersonalizationDictCache = new ConcurrentHashMap<>(); + @Nonnull public static UserHistoryDictionary getUserHistoryDictionary( - final Context context, final Locale locale) { - final String localeStr = locale.toString(); + final Context context, final Locale locale, @Nullable final String accountName) { + String lookupStr = locale.toString(); + if (accountName != null) { + lookupStr += "." + accountName; + } synchronized (sLangUserHistoryDictCache) { - if (sLangUserHistoryDictCache.containsKey(localeStr)) { + if (sLangUserHistoryDictCache.containsKey(lookupStr)) { final SoftReference<UserHistoryDictionary> ref = - sLangUserHistoryDictCache.get(localeStr); + sLangUserHistoryDictCache.get(lookupStr); final UserHistoryDictionary dict = ref == null ? null : ref.get(); if (dict != null) { if (DEBUG) { - Log.w(TAG, "Use cached UserHistoryDictionary for " + locale); + Log.d(TAG, "Use cached UserHistoryDictionary with lookup: " + lookupStr); } dict.reloadDictionaryIfRequired(); return dict; } } - final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale); - sLangUserHistoryDictCache.put(localeStr, new SoftReference<>(dict)); - return dict; - } - } - - private static int sCurrentTimestampForTesting = 0; - public static void currentTimeChangedForTesting(final int currentTimestamp) { - if (TimeUnit.MILLISECONDS.toSeconds( - DictionaryDecayBroadcastReciever.DICTIONARY_DECAY_INTERVAL) - < currentTimestamp - sCurrentTimestampForTesting) { - runGCOnAllOpenedUserHistoryDictionaries(); - runGCOnAllOpenedPersonalizationDictionaries(); - } - } - - public static void runGCOnAllOpenedUserHistoryDictionaries() { - runGCOnAllDictionariesIfRequired(sLangUserHistoryDictCache); - } - - public static void runGCOnAllOpenedPersonalizationDictionaries() { - runGCOnAllDictionariesIfRequired(sLangPersonalizationDictCache); - } - - private static <T extends DecayingExpandableBinaryDictionaryBase> - void runGCOnAllDictionariesIfRequired( - final ConcurrentHashMap<String, SoftReference<T>> dictionaryMap) { - for (final ConcurrentHashMap.Entry<String, SoftReference<T>> entry - : dictionaryMap.entrySet()) { - final DecayingExpandableBinaryDictionaryBase dict = entry.getValue().get(); - if (dict != null) { - dict.runGCIfRequired(); - } else { - dictionaryMap.remove(entry.getKey()); - } - } - } - - public static PersonalizationDictionary getPersonalizationDictionary( - final Context context, final Locale locale) { - final String localeStr = locale.toString(); - synchronized (sLangPersonalizationDictCache) { - if (sLangPersonalizationDictCache.containsKey(localeStr)) { - final SoftReference<PersonalizationDictionary> ref = - sLangPersonalizationDictCache.get(localeStr); - final PersonalizationDictionary dict = ref == null ? null : ref.get(); - if (dict != null) { - if (DEBUG) { - Log.w(TAG, "Use cached PersonalizationDictionary for " + locale); - } - return dict; - } - } - final PersonalizationDictionary dict = new PersonalizationDictionary(context, locale); - sLangPersonalizationDictCache.put(localeStr, new SoftReference<>(dict)); + final UserHistoryDictionary dict = new UserHistoryDictionary( + context, locale, accountName); + sLangUserHistoryDictCache.put(lookupStr, new SoftReference<>(dict)); return dict; } } - public static void removeAllPersonalizationDictionaries(final Context context) { - removeAllDictionaries(context, sLangPersonalizationDictCache, - PersonalizationDictionary.NAME); - } - public static void removeAllUserHistoryDictionaries(final Context context) { - removeAllDictionaries(context, sLangUserHistoryDictCache, - UserHistoryDictionary.NAME); - } - - private static <T extends DecayingExpandableBinaryDictionaryBase> void removeAllDictionaries( - final Context context, final ConcurrentHashMap<String, SoftReference<T>> dictionaryMap, - final String dictNamePrefix) { - synchronized (dictionaryMap) { - for (final ConcurrentHashMap.Entry<String, SoftReference<T>> entry - : dictionaryMap.entrySet()) { + synchronized (sLangUserHistoryDictCache) { + for (final ConcurrentHashMap.Entry<String, SoftReference<UserHistoryDictionary>> entry + : sLangUserHistoryDictCache.entrySet()) { if (entry.getValue() != null) { - final DecayingExpandableBinaryDictionaryBase dict = entry.getValue().get(); + final UserHistoryDictionary dict = entry.getValue().get(); if (dict != null) { dict.clear(); } } } - dictionaryMap.clear(); + sLangUserHistoryDictCache.clear(); final File filesDir = context.getFilesDir(); if (filesDir == null) { Log.e(TAG, "context.getFilesDir() returned null."); return; } - if (!FileUtils.deleteFilteredFiles(filesDir, new DictFilter(dictNamePrefix))) { - Log.e(TAG, "Cannot remove all existing dictionary files. filesDir: " - + filesDir.getAbsolutePath() + ", dictNamePrefix: " + dictNamePrefix); + final boolean filesDeleted = FileUtils.deleteFilteredFiles( + filesDir, new DictFilter(UserHistoryDictionary.NAME)); + if (!filesDeleted) { + Log.e(TAG, "Cannot remove dictionary files. filesDir: " + filesDir.getAbsolutePath() + + ", dictNamePrefix: " + UserHistoryDictionary.NAME); } } } diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java index 34d4d4ed7..cbf0829b5 100644 --- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java +++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java @@ -17,72 +17,120 @@ package com.android.inputmethod.latin.personalization; import android.content.Context; -import android.text.TextUtils; +import com.android.inputmethod.annotations.ExternallyReferenced; import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.BinaryDictionary; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.ExpandableBinaryDictionary; -import com.android.inputmethod.latin.PrevWordsInfo; -import com.android.inputmethod.latin.utils.DistracterFilter; +import com.android.inputmethod.latin.NgramContext; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; +import com.android.inputmethod.latin.define.ProductionFlags; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import java.io.File; import java.util.Locale; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** - * 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. + * Locally gathers statistics 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. */ -public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBase { - /* package */ static final String NAME = UserHistoryDictionary.class.getSimpleName(); +public class UserHistoryDictionary extends ExpandableBinaryDictionary { + static final String NAME = UserHistoryDictionary.class.getSimpleName(); // TODO: Make this constructor private - /* package */ UserHistoryDictionary(final Context context, final Locale locale) { - super(context, getDictName(NAME, locale, null /* dictFile */), locale, - Dictionary.TYPE_USER_HISTORY, null /* dictFile */); + UserHistoryDictionary(final Context context, final Locale locale, + @Nullable final String account) { + super(context, getUserHistoryDictName(NAME, locale, null /* dictFile */, account), locale, Dictionary.TYPE_USER_HISTORY, null); + if (mLocale != null && mLocale.toString().length() > 1) { + reloadDictionaryIfRequired(); + } } + /** + * @returns the name of the {@link UserHistoryDictionary}. + */ @UsedForTesting + static String getUserHistoryDictName(final String name, final Locale locale, + @Nullable final File dictFile, @Nullable final String account) { + if (!ProductionFlags.ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY) { + return getDictName(name, locale, dictFile); + } + return getUserHistoryDictNamePerAccount(name, locale, dictFile, account); + } + + /** + * Uses the currently signed in account to determine the dictionary name. + */ + private static String getUserHistoryDictNamePerAccount(final String name, final Locale locale, + @Nullable final File dictFile, @Nullable final String account) { + if (dictFile != null) { + return dictFile.getName(); + } + String dictName = name + "." + locale.toString(); + if (account != null) { + dictName += "." + account; + } + return dictName; + } + + // 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); + final File dictFile, final String dictNamePrefix, @Nullable final String account) { + return PersonalizationHelper.getUserHistoryDictionary(context, locale, account); } /** * 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) { + if (word.length() > BinaryDictionary.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.updateEntriesForWord(ngramContext, word, + isValid, 1 /* count */, timestamp); + } + + @Override + public void close() { + // Flush pending writes. + asyncFlushBinaryDictionary(); + super.close(); + } + + @Override + protected Map<String, String> getHeaderAttributeMap() { + final Map<String, String> attributeMap = super.getHeaderAttributeMap(); + attributeMap.put(DictionaryHeader.USES_FORGETTING_CURVE_KEY, + DictionaryHeader.ATTRIBUTE_VALUE_TRUE); + attributeMap.put(DictionaryHeader.HAS_HISTORICAL_INFO_KEY, + DictionaryHeader.ATTRIBUTE_VALUE_TRUE); + return attributeMap; + } + + @Override + protected void loadInitialContentsLocked() { + // No initial contents. + } + + @Override + public boolean isValidWord(final String word) { + // Strings out of this dictionary should not be considered existing words. + return false; } } 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..a2ae0ef4e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java @@ -0,0 +1,445 @@ +/* + * 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.DialogInterface.OnShowListener; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.TwoStatePreference; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.widget.ListView; +import android.widget.TextView; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.accounts.AccountStateChangedListener; +import com.android.inputmethod.latin.accounts.LoginAccountUtils; +import com.android.inputmethod.latin.define.ProductionFlags; +import com.android.inputmethod.latin.utils.ManagedProfileUtils; + +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_ENABLE_SYNC_NOW = "pref_enable_cloud_sync"; + private static final String PREF_SYNC_NOW = "pref_sync_now"; + private static final String PREF_CLEAR_SYNC_DATA = "pref_clear_sync_data"; + + static final String PREF_ACCCOUNT_SWITCHER = "account_switcher"; + + /** + * Onclick listener for sync now pref. + */ + private final Preference.OnPreferenceClickListener mSyncNowListener = + new SyncNowListener(); + /** + * Onclick listener for delete sync pref. + */ + private final Preference.OnPreferenceClickListener mDeleteSyncDataListener = + new DeleteSyncDataListener(); + + /** + * Onclick listener for enable sync pref. + */ + private final Preference.OnPreferenceClickListener mEnableSyncClickListener = + new EnableSyncClickListener(); + + /** + * Enable sync checkbox pref. + */ + private TwoStatePreference mEnableSyncPreference; + + /** + * Enable sync checkbox pref. + */ + private Preference mSyncNowPreference; + + /** + * Clear sync data pref. + */ + private Preference mClearSyncDataPreference; + + /** + * Account switcher preference. + */ + private Preference mAccountSwitcher; + + + @Override + public void onCreate(final Bundle icicle) { + super.onCreate(icicle); + addPreferencesFromResource(R.xml.prefs_screen_accounts); + + if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) { + final Preference enableMetricsLogging = + findPreference(Settings.PREF_ENABLE_METRICS_LOGGING); + final Resources res = getResources(); + 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_USER_HISTORY_DICTIONARY_SYNC) { + removeSyncPreferences(); + } else { + // Temporarily disable the preferences till we can + // check that we don't have a work profile. + disableSyncPreferences(); + new ManagedProfileCheckerTask(this).execute(); + } + } + + /** + * Task to check work profile. If found, it removes the sync prefs. If not, + * it enables them. + */ + private static class ManagedProfileCheckerTask extends AsyncTask<Void, Void, Void> { + private final AccountsSettingsFragment mFragment; + + private ManagedProfileCheckerTask(final AccountsSettingsFragment fragment) { + mFragment = fragment; + } + + @Override + protected Void doInBackground(Void... params) { + if (ManagedProfileUtils.getInstance().hasWorkProfile(mFragment.getActivity())) { + mFragment.removeSyncPreferences(); + } else { + mFragment.refreshAccountAndDependentPreferences( + mFragment.getSignedInAccountName()); + } + return null; + } + } + + private void enableSyncPreferences() { + mAccountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER); + if (mAccountSwitcher == null) { + // Preference has been removed because the device has a managed profile. + return; + } + mAccountSwitcher.setEnabled(true); + + mEnableSyncPreference = (TwoStatePreference) findPreference(PREF_ENABLE_SYNC_NOW); + mEnableSyncPreference.setEnabled(true); + mEnableSyncPreference.setOnPreferenceClickListener(mEnableSyncClickListener); + + mSyncNowPreference = findPreference(PREF_SYNC_NOW); + mSyncNowPreference.setEnabled(true); + mSyncNowPreference.setOnPreferenceClickListener(mSyncNowListener); + + mClearSyncDataPreference = findPreference(PREF_CLEAR_SYNC_DATA); + mSyncNowPreference.setEnabled(true); + mClearSyncDataPreference.setOnPreferenceClickListener(mDeleteSyncDataListener); + } + + private void disableSyncPreferences() { + mAccountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER); + if (mAccountSwitcher == null) { + // Preference has been removed because the device has a managed profile. + return; + } + mAccountSwitcher.setEnabled(false); + + mEnableSyncPreference = (TwoStatePreference) findPreference(PREF_ENABLE_SYNC_NOW); + mEnableSyncPreference.setEnabled(false); + + mSyncNowPreference = findPreference(PREF_SYNC_NOW); + mSyncNowPreference.setEnabled(false); + + mClearSyncDataPreference = findPreference(PREF_CLEAR_SYNC_DATA); + mSyncNowPreference.setEnabled(false); + } + + private void removeSyncPreferences() { + removePreference(PREF_ACCCOUNT_SWITCHER); + removePreference(PREF_ENABLE_CLOUD_SYNC); + removePreference(PREF_SYNC_NOW); + removePreference(PREF_CLEAR_SYNC_DATA); + } + + @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); + mEnableSyncPreference = (TwoStatePreference) findPreference(PREF_ENABLE_SYNC_NOW); + if (syncEnabled) { + mEnableSyncPreference.setSummary(R.string.cloud_sync_summary); + } else { + mEnableSyncPreference.setSummary(R.string.cloud_sync_summary_disabled); + } + AccountStateChangedListener.onSyncPreferenceChanged(getSignedInAccountName(), + syncEnabled); + } + } + + /** + * Summarizes what account is being used and turns off dependent preferences if no account + * is currently selected. + */ + private void refreshAccountAndDependentPreferences(@Nullable final String currentAccount) { + // TODO(cvnguyen): Write tests. + if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) { + return; + } + + final String[] accountsForLogin = + LoginAccountUtils.getAccountsForLogin(getActivity()); + + if (accountsForLogin.length > 0) { + enableSyncPreferences(); + if (mAccountSwitcher == null) { + return; + } + mAccountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(final Preference preference) { + if (accountsForLogin.length > 0) { + // TODO: Add addition of account. + createAccountPicker(accountsForLogin, currentAccount, + new AccountChangedListener(null)).show(); + } + return true; + } + }); + } else { + mAccountSwitcher.setEnabled(false); + disableSyncPreferences(); + mEnableSyncPreference.setSummary(getString(R.string.add_account_to_enable_sync)); + } + + if (currentAccount == null) { + // No account is currently selected; switch enable sync preference off. + mAccountSwitcher.setSummary(getString(R.string.no_accounts_selected)); + mEnableSyncPreference.setChecked(false); + } else { + // Set the currently selected account as the summary text. + mAccountSwitcher.setSummary(getString(R.string.account_selected, currentAccount)); + } + } + + @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. + * + * @param accounts list of accounts on the device. + * @param selectedAccount currently selected account + * @param positiveButtonClickListener listener that gets called when positive button is + * clicked + */ + @UsedForTesting + AlertDialog createAccountPicker(final String[] accounts, + final String selectedAccount, + final DialogInterface.OnClickListener positiveButtonClickListener) { + 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, positiveButtonClickListener) + .setNegativeButton(R.string.account_select_cancel, null); + if (isSignedIn) { + builder.setNeutralButton(R.string.account_select_sign_out, positiveButtonClickListener); + } + 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 { + /** + * Represents preference that should be changed based on account chosen. + */ + private TwoStatePreference mDependentPreference; + + AccountChangedListener(final TwoStatePreference dependentPreference) { + mDependentPreference = dependentPreference; + } + + @Override + public void onClick(final DialogInterface dialog, final 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); + if (mDependentPreference != null) { + mDependentPreference.setChecked(true); + } + 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; + } + } + + /** + * Listener that initiates the process of deleting user's data from the cloud. + */ + class DeleteSyncDataListener implements Preference.OnPreferenceClickListener { + @Override + public boolean onPreferenceClick(final Preference preference) { + final AlertDialog confirmationDialog = new AlertDialog.Builder(getActivity()) + .setTitle(R.string.clear_sync_data_title) + .setMessage(R.string.clear_sync_data_confirmation) + .setPositiveButton(R.string.clear_sync_data_ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + AccountStateChangedListener.forceDelete( + getSignedInAccountName()); + } + } + }) + .setNegativeButton(R.string.cloud_sync_cancel, null /* OnClickListener */) + .create(); + confirmationDialog.show(); + return true; + } + } + + /** + * Listens to events when user clicks on "Enable sync" feature. + */ + class EnableSyncClickListener implements OnShowListener, Preference.OnPreferenceClickListener { + // TODO(cvnguyen): Write tests. + @Override + public boolean onPreferenceClick(final Preference preference) { + final TwoStatePreference syncPreference = (TwoStatePreference) preference; + if (syncPreference.isChecked()) { + // Uncheck for now. + syncPreference.setChecked(false); + + // Show opt-in. + final AlertDialog optInDialog = new AlertDialog.Builder(getActivity()) + .setTitle(R.string.cloud_sync_title) + .setMessage(R.string.cloud_sync_opt_in_text) + .setPositiveButton(R.string.account_select_ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, + final int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + final Context context = getActivity(); + final String[] accountsForLogin = + LoginAccountUtils.getAccountsForLogin(context); + createAccountPicker(accountsForLogin, + getSignedInAccountName(), + new AccountChangedListener(syncPreference)) + .show(); + } + } + }) + .setNegativeButton(R.string.cloud_sync_cancel, null) + .create(); + optInDialog.setOnShowListener(this); + optInDialog.show(); + } + return true; + } + + @Override + public void onShow(DialogInterface dialog) { + TextView messageView = (TextView) ((AlertDialog) dialog).findViewById( + android.R.id.message); + if (messageView != null) { + messageView.setMovementMethod(LinkMovementMethod.getInstance()); + } + } + } +} diff --git a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java index 00f2c73dd..f2e1aed4c 100644 --- a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java @@ -23,12 +23,10 @@ import android.media.AudioManager; import android.os.Bundle; import android.preference.ListPreference; import android.preference.Preference; -import android.preference.TwoStatePreference; import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.define.ProductionFlags; -import com.android.inputmethod.latin.setup.LauncherIconVisibilityManager; /** * "Advanced" settings sub screen. @@ -89,26 +87,9 @@ public final class AdvancedSettingsFragment extends SubScreenFragment { Settings.readKeyPreviewPopupEnabled(prefs, res)); } - if (!res.getBoolean(R.bool.config_setup_wizard_available)) { - removePreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON); - } - - 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); - enableMetricsLogging.setTitle(enableMetricsLoggingTitle); - } - } else { - removePreference(Settings.PREF_ENABLE_METRICS_LOGGING); - } - setupKeypressVibrationDurationSettings(); setupKeypressSoundVolumeSettings(); + setupKeyLongpressTimeoutSettings(); refreshEnablingsOfKeypressSoundAndVibrationSettings(); } @@ -116,11 +97,6 @@ public final class AdvancedSettingsFragment extends SubScreenFragment { public void onResume() { super.onResume(); final SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); - final TwoStatePreference showSetupWizardIcon = - (TwoStatePreference)findPreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON); - if (showSetupWizardIcon != null) { - showSetupWizardIcon.setChecked(Settings.readShowSetupWizardIcon(prefs, getActivity())); - } updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); } @@ -130,8 +106,6 @@ public final class AdvancedSettingsFragment extends SubScreenFragment { if (key.equals(Settings.PREF_POPUP_ON)) { setPreferenceEnabled(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, Settings.readKeyPreviewPopupEnabled(prefs, res)); - } else if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) { - LauncherIconVisibilityManager.updateSetupWizardIconVisibility(getActivity()); } updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); refreshEnablingsOfKeypressSoundAndVibrationSettings(); @@ -245,4 +219,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/CorrectionSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java index ec29a7eb2..62834cd2a 100644 --- a/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java @@ -24,8 +24,8 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Build; import android.os.Bundle; -import android.preference.ListPreference; import android.preference.Preference; +import android.preference.TwoStatePreference; import com.android.inputmethod.dictionarypack.DictionarySettingsActivity; import com.android.inputmethod.latin.R; @@ -49,7 +49,7 @@ import java.util.TreeSet; */ public final class CorrectionSettingsFragment extends SubScreenFragment { private static final boolean DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS = false; - private static final boolean USE_INTERNAL_PERSONAL_DICTIONARY_SETTIGS = + private static final boolean USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS = DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS || Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2; @@ -61,8 +61,6 @@ public final class CorrectionSettingsFragment extends SubScreenFragment { final Context context = getActivity(); final PackageManager pm = context.getPackageManager(); - ensureConsistencyOfAutoCorrectionSettings(); - final Preference dictionaryLink = findPreference(Settings.PREF_CONFIGURE_DICTIONARIES_KEY); final Intent intent = dictionaryLink.getIntent(); intent.setClassName(context.getPackageName(), DictionarySettingsActivity.class.getName()); @@ -74,7 +72,7 @@ public final class CorrectionSettingsFragment extends SubScreenFragment { final Preference editPersonalDictionary = findPreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY); final Intent editPersonalDictionaryIntent = editPersonalDictionary.getIntent(); - final ResolveInfo ri = USE_INTERNAL_PERSONAL_DICTIONARY_SETTIGS ? null + final ResolveInfo ri = USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS ? null : pm.resolveActivity( editPersonalDictionaryIntent, PackageManager.MATCH_DEFAULT_ONLY); if (ri == null) { @@ -82,21 +80,6 @@ public final class CorrectionSettingsFragment extends SubScreenFragment { } } - @Override - public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { - ensureConsistencyOfAutoCorrectionSettings(); - } - - private void ensureConsistencyOfAutoCorrectionSettings() { - final String autoCorrectionOff = getString( - R.string.auto_correction_threshold_mode_index_off); - final ListPreference autoCorrectionThresholdPref = (ListPreference)findPreference( - Settings.PREF_AUTO_CORRECTION_THRESHOLD); - final String currentSetting = autoCorrectionThresholdPref.getValue(); - setPreferenceEnabled( - Settings.PREF_BIGRAM_PREDICTIONS, !currentSetting.equals(autoCorrectionOff)); - } - private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) { final Activity activity = getActivity(); final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(activity); 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..21ea8f859 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/settings/CustomInputStylePreference.java @@ -0,0 +1,341 @@ +/* + * 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; + } + } + + @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; + 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()); + setSubtype(myState.mSubtype); + } + + static final class SavedState extends Preference.BaseSavedState { + InputMethodSubtype mSubtype; + + public SavedState(final Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelable(mSubtype, 0); + } + + public SavedState(final Parcel source) { + super(source); + 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); + + final String[] predefinedKeyboardLayoutSet = context.getResources().getStringArray( + R.array.predefined_layouts); + // TODO: Should filter out already existing combinations of locale and layout. + for (final String layout : predefinedKeyboardLayoutSet) { + // 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..37855377d 100644 --- a/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java @@ -28,9 +28,8 @@ import android.preference.PreferenceGroup; import android.preference.TwoStatePreference; import com.android.inputmethod.latin.DictionaryDumpBroadcastReceiver; -import com.android.inputmethod.latin.DictionaryFacilitator; +import com.android.inputmethod.latin.DictionaryFacilitatorImpl; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.debug.ExternalDictionaryGetterForDebug; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.ResourceUtils; @@ -43,12 +42,10 @@ import java.util.Locale; */ public final class DebugSettingsFragment extends SubScreenFragment implements OnPreferenceClickListener { - private static final String PREF_READ_EXTERNAL_DICTIONARY = "read_external_dictionary"; private static final String PREF_KEY_DUMP_DICTS = "pref_key_dump_dictionaries"; private static final String PREF_KEY_DUMP_DICT_PREFIX = "pref_key_dump_dictionaries"; private boolean mServiceNeedsRestart = false; - private Preference mReadExternalDictionaryPref; private TwoStatePreference mDebugMode; @Override @@ -56,24 +53,18 @@ 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); - } - - mReadExternalDictionaryPref = findPreference(PREF_READ_EXTERNAL_DICTIONARY); - if (mReadExternalDictionaryPref != null) { - mReadExternalDictionaryPref.setOnPreferenceClickListener(this); + if (!Settings.SHOULD_SHOW_LXX_SUGGESTION_UI) { + removePreference(DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI); } final PreferenceGroup dictDumpPreferenceGroup = (PreferenceGroup)findPreference(PREF_KEY_DUMP_DICTS); - for (final String dictName : DictionaryFacilitator.DICT_TYPE_TO_CLASS.keySet()) { + for (final String dictName : DictionaryFacilitatorImpl.DICT_TYPE_TO_CLASS.keySet()) { final Preference pref = new DictDumpPreference(getActivity(), dictName); pref.setOnPreferenceClickListener(this); 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 +81,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); @@ -110,11 +103,6 @@ public final class DebugSettingsFragment extends SubScreenFragment @Override public boolean onPreferenceClick(final Preference pref) { final Context context = getActivity(); - if (pref == mReadExternalDictionaryPref) { - ExternalDictionaryGetterForDebug.chooseAndInstallDictionary(context); - mServiceNeedsRestart = true; - return true; - } if (pref instanceof DictDumpPreference) { final DictDumpPreference dictDumpPref = (DictDumpPreference)pref; final String dictName = dictDumpPref.mDictName; @@ -143,8 +131,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 +150,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 +180,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 +202,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 +210,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 +222,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 +240,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 +268,17 @@ public final class DebugSettingsFragment extends SubScreenFragment @Override public int readValue(final String key) { - return Settings.readKeyPreviewAnimationDuration(prefs, key, defaultValue); + return getPercentageFromValue(Settings.readKeyboardHeight(prefs, 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 deleted file mode 100644 index b073c50a4..000000000 --- a/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java +++ /dev/null @@ -1,42 +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.settings; - -import android.os.Bundle; - -import com.android.inputmethod.latin.R; - -import java.util.ArrayList; - -/** - * "Multilingual options" settings sub screen. - * - * This settings sub screen handles the following input preferences. - * - Language switch key - * - Switch to other input methods - */ -public final class MultiLingualSettingsFragment extends SubScreenFragment { - @Override - public void onCreate(final Bundle icicle) { - super.onCreate(icicle); - addPreferencesFromResource(R.xml.prefs_screen_multilingual); - if (!Settings.ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS) { - removePreference(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY); - removePreference(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST); - } - } -} diff --git a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java deleted file mode 100644 index 31a20c4db..000000000 --- a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2013 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; - -public class NativeSuggestOptions { - // Need to update suggest_options.h when you add, remove or reorder options. - private static final int IS_GESTURE = 0; - 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 final int[] mOptions = new int[OPTIONS_SIZE - + AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE]; - - public void setIsGesture(final boolean value) { - setBooleanOption(IS_GESTURE, value); - } - - public void setUseFullEditDistance(final boolean value) { - setBooleanOption(USE_FULL_EDIT_DISTANCE, value); - } - - public void setBlockOffensiveWords(final boolean value) { - setBooleanOption(BLOCK_OFFENSIVE_WORDS, value); - } - - public void setSpaceAwareGestureEnabled(final boolean value) { - setBooleanOption(SPACE_AWARE_GESTURE_ENABLED, value); - } - - public void setAdditionalFeaturesOptions(final int[] additionalOptions) { - if (additionalOptions == null) { - return; - } - for (int i = 0; i < additionalOptions.length; i++) { - setIntegerOption(OPTIONS_SIZE + i, additionalOptions[i]); - } - } - - public int[] getOptions() { - return mOptions; - } - - private void setBooleanOption(final int key, final boolean value) { - mOptions[key] = value ? 1 : 0; - } - - private void setIntegerOption(final int key, final int value) { - mOptions[key] = value; - } -} diff --git a/java/src/com/android/inputmethod/latin/settings/PreferencesSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/PreferencesSettingsFragment.java index 49db2bdc0..d9858e61f 100644 --- a/java/src/com/android/inputmethod/latin/settings/PreferencesSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/PreferencesSettingsFragment.java @@ -19,12 +19,13 @@ package com.android.inputmethod.latin.settings; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; +import android.os.Build; import android.os.Bundle; import android.preference.Preference; import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.SubtypeSwitcher; +import com.android.inputmethod.latin.RichInputMethodManager; /** * "Preferences" settings sub screen. @@ -38,6 +39,10 @@ import com.android.inputmethod.latin.SubtypeSwitcher; * - Voice input key */ public final class PreferencesSettingsFragment extends SubScreenFragment { + + private static final boolean VOICE_IME_ENABLED = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + @Override public void onCreate(final Bundle icicle) { super.onCreate(icicle); @@ -49,7 +54,7 @@ public final class PreferencesSettingsFragment extends SubScreenFragment { // 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); + RichInputMethodManager.init(context); final boolean showVoiceKeyOption = res.getBoolean( R.bool.config_enable_show_voice_key_option); @@ -71,11 +76,10 @@ public final class PreferencesSettingsFragment extends SubScreenFragment { super.onResume(); final Preference voiceInputKeyOption = findPreference(Settings.PREF_VOICE_INPUT_KEY); if (voiceInputKeyOption != null) { - final boolean isShortcutImeEnabled = SubtypeSwitcher.getInstance() - .isShortcutImeEnabled(); - voiceInputKeyOption.setEnabled(isShortcutImeEnabled); - voiceInputKeyOption.setSummary( - isShortcutImeEnabled ? null : getText(R.string.voice_input_disabled_summary)); + RichInputMethodManager.getInstance().refreshSubtypeCaches(); + voiceInputKeyOption.setEnabled(VOICE_IME_ENABLED); + voiceInputKeyOption.setSummary(VOICE_IME_ENABLED + ? null : getText(R.string.voice_input_disabled_summary)); } } 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..715f7bb38 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -18,7 +18,6 @@ package com.android.inputmethod.latin.settings; import android.content.Context; import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Build; @@ -29,26 +28,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_APPEARANCE = "screen_appearance"; + public static final String SCREEN_ACCOUNTS = "screen_accounts"; public static final String SCREEN_THEME = "screen_theme"; - public static final String SCREEN_MULTILINGUAL = "screen_multilingual"; - public static final String SCREEN_GESTURE = "screen_gesture"; - public static final String SCREEN_CORRECTION = "screen_correction"; - public static final String SCREEN_ADVANCED = "screen_advanced"; public static final String SCREEN_DEBUG = "screen_debug"; // In the same order as xml/prefs.xml public static final String PREF_AUTO_CAP = "auto_cap"; @@ -60,7 +57,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key"; public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary"; public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key"; - public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold"; + // PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE is obsolete. Use PREF_AUTO_CORRECTION instead. + public static final String PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE = + "auto_correction_threshold"; + public static final String PREF_AUTO_CORRECTION = "pref_key_auto_correction"; // PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE is obsolete. Use PREF_SHOW_SUGGESTIONS instead. public static final String PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE = "show_suggestions_setting"; public static final String PREF_SHOW_SUGGESTIONS = "show_suggestions"; @@ -70,19 +70,16 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang "pref_key_use_double_space_period"; public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE = "pref_key_block_potentially_offensive"; - // No multilingual options in Android L and above for now. - public static final boolean SHOW_MULTILINGUAL_SETTINGS = - 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,20 +87,17 @@ 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"; - public static final String PREF_SHOW_SETUP_WIZARD_ICON = "pref_show_setup_wizard_icon"; - public static final String PREF_PHRASE_GESTURE_ENABLED = "pref_gesture_space_aware"; - public static final String PREF_INPUT_LANGUAGE = "input_language"; - public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; 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 = @@ -149,6 +143,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang mRes = context.getResources(); mPrefs = PreferenceManager.getDefaultSharedPreferences(context); mPrefs.registerOnSharedPreferenceChangeListener(this); + upgradeAutocorrectionSettings(mPrefs, mRes); } public void onDestroy() { @@ -166,13 +161,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 { @@ -198,12 +194,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return mSettingsValues.mIsInternal; } - public boolean isWordSeparator(final int code) { - return mSettingsValues.isWordSeparator(code); - } - - public boolean getBlockPotentiallyOffensive() { - 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 @@ -220,11 +212,13 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang res.getBoolean(R.bool.config_default_vibration_enabled)); } - public static boolean readAutoCorrectEnabled(final String currentAutoCorrectionSetting, + public static boolean readAutoCorrectEnabled(final SharedPreferences prefs, final Resources res) { - final String autoCorrectionOff = res.getString( - R.string.auto_correction_threshold_mode_index_off); - return !currentAutoCorrectionSetting.equals(autoCorrectionOff); + return prefs.getBoolean(PREF_AUTO_CORRECTION, true); + } + + public static float readPlausibilityThreshold(final Resources res) { + return Float.parseFloat(res.getString(R.string.plausibility_threshold)); } public static boolean readBlockPotentiallyOffensive(final SharedPreferences prefs, @@ -243,12 +237,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang && prefs.getBoolean(PREF_GESTURE_INPUT, true); } - public static boolean readPhraseGestureEnabled(final SharedPreferences prefs, - final Resources res) { - return prefs.getBoolean(PREF_PHRASE_GESTURE_ENABLED, - res.getBoolean(R.bool.config_default_phrase_gesture_enabled)); - } - public static boolean readFromBuildConfigIfToShowKeyPreviewPopupOption(final Resources res) { return res.getBoolean(R.bool.config_enable_show_key_preview_popup_option); } @@ -314,7 +302,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,25 +340,15 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds : defaultValue; } - public static boolean readUseFullscreenMode(final Resources res) { - return res.getBoolean(R.bool.config_use_fullscreen_mode); + public static float readKeyboardHeight(final SharedPreferences prefs, + final float defaultValue) { + final float percentage = prefs.getFloat( + DebugSettings.PREF_KEYBOARD_HEIGHT_SCALE, UNDEFINED_PREFERENCE_VALUE_FLOAT); + return (percentage != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? percentage : defaultValue; } - public static boolean readShowSetupWizardIcon(final SharedPreferences prefs, - final Context context) { - final boolean enableSetupWizardByConfig = context.getResources().getBoolean( - R.bool.config_setup_wizard_available); - if (!enableSetupWizardByConfig) { - return false; - } - if (!prefs.contains(PREF_SHOW_SETUP_WIZARD_ICON)) { - final ApplicationInfo appInfo = context.getApplicationInfo(); - final boolean isApplicationInSystemImage = - (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - // Default value - return !isApplicationInSystemImage; - } - return prefs.getBoolean(PREF_SHOW_SETUP_WIZARD_ICON, false); + public static boolean readUseFullscreenMode(final Resources res) { + return res.getBoolean(R.bool.config_use_fullscreen_mode); } public static boolean readHasHardwareKeyboard(final Configuration conf) { @@ -446,4 +424,21 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang final SharedPreferences prefs, final int defValue) { return prefs.getInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_ID, defValue); } + + private void upgradeAutocorrectionSettings(final SharedPreferences prefs, final Resources res) { + final String thresholdSetting = + prefs.getString(PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE, null); + if (thresholdSetting != null) { + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE); + final String autoCorrectionOff = + res.getString(R.string.auto_correction_threshold_mode_index_off); + if (thresholdSetting.equals(autoCorrectionOff)) { + editor.putBoolean(PREF_AUTO_CORRECTION, false); + } else { + editor.putBoolean(PREF_AUTO_CORRECTION, true); + } + editor.commit(); + } + } } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java b/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java index b0c494098..9975277e4 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java @@ -17,6 +17,8 @@ package com.android.inputmethod.latin.settings; import com.android.inputmethod.latin.utils.FragmentUtils; +import com.android.inputmethod.latin.utils.StatsUtils; +import com.android.inputmethod.latin.utils.StatsUtilsManager; import android.app.ActionBar; import android.content.Intent; @@ -25,19 +27,30 @@ import android.preference.PreferenceActivity; import android.view.MenuItem; public final class SettingsActivity extends PreferenceActivity { - public static final String EXTRA_SHOW_HOME_AS_UP = "show_home_as_up"; private static final String DEFAULT_FRAGMENT = SettingsFragment.class.getName(); + + public static final String EXTRA_SHOW_HOME_AS_UP = "show_home_as_up"; + public static final String EXTRA_ENTRY_KEY = "entry"; + public static final String EXTRA_ENTRY_VALUE_LONG_PRESS_COMMA = "long_press_comma"; + public static final String EXTRA_ENTRY_VALUE_APP_ICON = "app_icon"; + public static final String EXTRA_ENTRY_VALUE_NOTICE_DIALOG = "important_notice"; + public static final String EXTRA_ENTRY_VALUE_SYSTEM_SETTINGS = "system_settings"; + private boolean mShowHomeAsUp; @Override protected void onCreate(final Bundle savedState) { super.onCreate(savedState); final ActionBar actionBar = getActionBar(); + final Intent intent = getIntent(); if (actionBar != null) { - mShowHomeAsUp = getIntent().getBooleanExtra(EXTRA_SHOW_HOME_AS_UP, true); + mShowHomeAsUp = intent.getBooleanExtra(EXTRA_SHOW_HOME_AS_UP, true); actionBar.setDisplayHomeAsUpEnabled(mShowHomeAsUp); actionBar.setHomeButtonEnabled(mShowHomeAsUp); } + StatsUtils.onSettingsActivity( + intent.hasExtra(EXTRA_ENTRY_KEY) ? intent.getStringExtra(EXTRA_ENTRY_KEY) + : EXTRA_ENTRY_VALUE_SYSTEM_SETTINGS); } @Override diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java index 6c8ab573a..f5455e3db 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java @@ -27,6 +27,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; @@ -49,9 +50,9 @@ public final class SettingsFragment extends InputMethodSettingsFragment { final PreferenceScreen preferenceScreen = getPreferenceScreen(); preferenceScreen.setTitle( ApplicationUtils.getActivityTitleResId(getActivity(), SettingsActivity.class)); - if (!Settings.SHOW_MULTILINGUAL_SETTINGS) { - 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); } } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index d8c548d8b..d112e7200 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -21,6 +21,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.os.Build; import android.util.Log; import android.view.inputmethod.EditorInfo; @@ -28,7 +29,6 @@ import com.android.inputmethod.compat.AppWorkaroundsUtils; import com.android.inputmethod.latin.InputAttributes; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; -import com.android.inputmethod.latin.SubtypeSwitcher; import com.android.inputmethod.latin.utils.AsyncResultHolder; import com.android.inputmethod.latin.utils.ResourceUtils; import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask; @@ -36,17 +36,22 @@ import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask; import java.util.Arrays; import java.util.Locale; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * 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; @@ -74,12 +79,17 @@ public final class SettingsValues { public final boolean mGestureTrailEnabled; public final boolean mGestureFloatingPreviewTextEnabled; public final boolean mSlidingKeyInputPreviewEnabled; - public final boolean mPhraseGestureEnabled; public final int mKeyLongpressTimeout; + public final boolean mEnableEmojiAltPhysicalKey; + public final boolean mCloudSyncEnabled; 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 @@ -88,20 +98,16 @@ public final class SettingsValues { public final int mKeyPreviewPopupDismissDelay; private final boolean mAutoCorrectEnabled; public final float mAutoCorrectionThreshold; + public final float mPlausibilityThreshold; public final boolean mAutoCorrectionEnabledPerUserSettings; private final boolean mSuggestionsEnabledPerUserSettings; private final AsyncResultHolder<AppWorkaroundsUtils> mAppWorkarounds; - // Setting values for additional features - public final int[] mAdditionalFeaturesSettingValues = - new int[AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE]; - - // TextDecorator - public final int mTextHighlightColorForAddToDictionaryIndicator; - // 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; @@ -109,8 +115,10 @@ public final class SettingsValues { public final float mKeyPreviewDismissEndXScale; public final float mKeyPreviewDismissEndYScale; + @Nullable public final String mAccount; + 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 +126,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); @@ -134,10 +137,7 @@ public final class SettingsValues { DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW, true); mShowsVoiceInputKey = needsToShowVoiceInputKey(prefs, res) && mInputAttributes.mShouldShowVoiceInputKey - && SubtypeSwitcher.getInstance().isShortcutImeEnabled(); - final String autoCorrectionThresholdRawValue = prefs.getString( - Settings.PREF_AUTO_CORRECTION_THRESHOLD, - res.getString(R.string.auto_correction_threshold_mode_index_modest)); + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; mIncludesOtherImesInLanguageSwitchList = Settings.ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS ? prefs.getBoolean(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false) : true /* forcibly */; @@ -148,35 +148,44 @@ public final class SettingsValues { mUseDoubleSpacePeriod = prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true) && inputAttributes.mIsGeneralTextInput; mBlockPotentiallyOffensive = Settings.readBlockPotentiallyOffensive(prefs, res); - mAutoCorrectEnabled = Settings.readAutoCorrectEnabled(autoCorrectionThresholdRawValue, res); + mAutoCorrectEnabled = Settings.readAutoCorrectEnabled(prefs, res); + final String autoCorrectionThresholdRawValue = mAutoCorrectEnabled + ? res.getString(R.string.auto_correction_threshold_mode_index_modest) + : res.getString(R.string.auto_correction_threshold_mode_index_off); mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res); 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); + mPlausibilityThreshold = Settings.readPlausibilityThreshold(res); mGestureInputEnabled = Settings.readGestureInputEnabled(prefs, res); mGestureTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true); - mGestureFloatingPreviewTextEnabled = prefs.getBoolean( - Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true); - mPhraseGestureEnabled = Settings.readPhraseGestureEnabled(prefs, res); + mCloudSyncEnabled = prefs.getBoolean(LocalSettingsConstants.PREF_ENABLE_CLOUD_SYNC, false); + mAccount = prefs.getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, + null /* default */); + mGestureFloatingPreviewTextEnabled = !mInputAttributes.mDisableGestureFloatingPreviewText + && prefs.getBoolean(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true); mAutoCorrectionEnabledPerUserSettings = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; mSuggestionsEnabledPerUserSettings = readSuggestionsEnabled(prefs); - AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray( - 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, 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 +220,10 @@ public final class SettingsValues { } } + public boolean isMetricsLoggingEnabled() { + return mEnableMetricsLogging; + } + public boolean isApplicationSpecifiedCompletionsOn() { return mInputAttributes.mApplicationSpecifiedCompletionOn; } @@ -224,6 +237,10 @@ public final class SettingsValues { return mSuggestionsEnabledPerUserSettings; } + public boolean isPersonalizationEnabled() { + return mUsePersonalizedDicts; + } + public boolean isWordSeparator(final int code) { return mSpacingAndPunctuations.isWordSeparator(code); } @@ -256,9 +273,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) { @@ -388,8 +404,6 @@ public final class SettingsValues { sb.append("" + mGestureFloatingPreviewTextEnabled); sb.append("\n mSlidingKeyInputPreviewEnabled = "); sb.append("" + mSlidingKeyInputPreviewEnabled); - sb.append("\n mPhraseGestureEnabled = "); - sb.append("" + mPhraseGestureEnabled); sb.append("\n mKeyLongpressTimeout = "); sb.append("" + mKeyLongpressTimeout); sb.append("\n mLocale = "); @@ -415,10 +429,6 @@ public final class SettingsValues { sb.append("\n mAppWorkarounds = "); final AppWorkaroundsUtils awu = mAppWorkarounds.get(null, 0); sb.append("" + (null == awu ? "null" : awu.toString())); - sb.append("\n mAdditionalFeaturesSettingValues = "); - sb.append("" + Arrays.toString(mAdditionalFeaturesSettingValues)); - sb.append("\n mTextHighlightColorForAddToDictionaryIndicator = "); - sb.append("" + mTextHighlightColorForAddToDictionaryIndicator); sb.append("\n mIsInternal = "); sb.append("" + mIsInternal); sb.append("\n mKeyPreviewShowUpDuration = "); diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValuesForSuggestion.java b/java/src/com/android/inputmethod/latin/settings/SettingsValuesForSuggestion.java index d80af4ba7..5e2e5a5d6 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValuesForSuggestion.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValuesForSuggestion.java @@ -18,13 +18,8 @@ package com.android.inputmethod.latin.settings; public class SettingsValuesForSuggestion { public final boolean mBlockPotentiallyOffensive; - public final boolean mSpaceAwareGestureEnabled; - public final int[] mAdditionalFeaturesSettingValues; - public SettingsValuesForSuggestion(final boolean blockPotentiallyOffensive, - final boolean spaceAwareGestureEnabled, final int[] additionalFeaturesSettingValues) { + public SettingsValuesForSuggestion(final boolean blockPotentiallyOffensive) { mBlockPotentiallyOffensive = blockPotentiallyOffensive; - mSpaceAwareGestureEnabled = spaceAwareGestureEnabled; - mAdditionalFeaturesSettingValues = additionalFeaturesSettingValues; } } 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/LauncherIconVisibilityManager.java b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java deleted file mode 100644 index 3f0b10225..000000000 --- a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2013 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.setup; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.preference.PreferenceManager; -import android.util.Log; - -import com.android.inputmethod.latin.settings.Settings; - -/** - * This class handles the {@link Intent#ACTION_MY_PACKAGE_REPLACED} broadcast intent when this IME - * package has been replaced by a newer version of the same package. This class also handles - * {@link Intent#ACTION_BOOT_COMPLETED} and {@link Intent#ACTION_USER_INITIALIZE} broadcast intent. - * - * If this IME has already been installed in the system image and a new version of this IME has - * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is received to this class to hide the - * setup wizard's icon. - * - * If this IME has already been installed in the data partition and a new version of this IME has - * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is forwarded to this class but it - * will not hide the setup wizard's icon, and the icon will appear on the launcher. - * - * If this IME hasn't been installed yet and has been newly installed, no - * {@link Intent#ACTION_MY_PACKAGE_REPLACED} will be sent and the setup wizard's icon will appear - * on the launcher. - * - * When the device has been booted, {@link Intent#ACTION_BOOT_COMPLETED} is forwarded to this class - * to check whether the setup wizard's icon should be appeared or not on the launcher - * depending on which partition this IME is installed. - * - * When a multiuser account has been created, {@link Intent#ACTION_USER_INITIALIZE} is forwarded to - * this class to check whether the setup wizard's icon should be appeared or not on the launcher - * depending on which partition this IME is installed. - */ -public final class LauncherIconVisibilityManager { - private static final String TAG = LauncherIconVisibilityManager.class.getSimpleName(); - - public static void updateSetupWizardIconVisibility(final Context context) { - final ComponentName setupWizardActivity = new ComponentName(context, SetupActivity.class); - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - final boolean stateHasSet; - if (Settings.readShowSetupWizardIcon(prefs, context)) { - stateHasSet = setActivityState(context, setupWizardActivity, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED); - Log.i(TAG, (stateHasSet ? "Enable activity: " : "Activity has already been enabled: ") - + setupWizardActivity); - } else { - stateHasSet = setActivityState(context, setupWizardActivity, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED); - Log.i(TAG, (stateHasSet ? "Disable activity: " : "Activity has already been disabled: ") - + setupWizardActivity); - } - } - - private static boolean setActivityState(final Context context, - final ComponentName activityComponent, final int activityState) { - final PackageManager pm = context.getPackageManager(); - final int activityComponentState = pm.getComponentEnabledSetting(activityComponent); - if (activityComponentState == activityState) { - return false; - } - pm.setComponentEnabledSetting( - activityComponent, activityState, PackageManager.DONT_KILL_APP); - return true; - } -} 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..bee22afd5 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; @@ -261,6 +265,8 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL intent.setClass(this, SettingsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.putExtra(SettingsActivity.EXTRA_ENTRY_KEY, + SettingsActivity.EXTRA_ENTRY_VALUE_APP_ICON); startActivity(intent); } @@ -304,6 +310,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..00f69f158 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -16,55 +16,37 @@ 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; +import android.util.Log; 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.SuggestedWords; +import com.android.inputmethod.latin.common.ComposedData; 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; + +import javax.annotation.Nonnull; /** * Service for spell checking, using LatinIME's dictionaries and mechanisms. @@ -72,72 +54,36 @@ import java.util.concurrent.TimeUnit; public final class AndroidSpellCheckerService extends SpellCheckerService implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = AndroidSpellCheckerService.class.getSimpleName(); - private static final boolean DBG = false; + private static final boolean DEBUG = 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 */, 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(); + new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */); public static final String SINGLE_QUOTE = "\u0027"; public static final String APOSTROPHE = "\u2019"; + private UserDictionaryLookup mUserDictionaryLookup; public AndroidSpellCheckerService() { super(); @@ -146,13 +92,33 @@ public final class AndroidSpellCheckerService extends SpellCheckerService } } - @Override public void onCreate() { + @Override + public void onCreate() { super.onCreate(); mRecommendedThreshold = Float.parseFloat(getString(R.string.spellchecker_recommended_threshold_value)); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); prefs.registerOnSharedPreferenceChangeListener(this); onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY); + // Create a UserDictionaryLookup. It needs to be close()d and set to null in onDestroy. + if (mUserDictionaryLookup == null) { + if (DEBUG) { + Log.d(TAG, "Creating mUserDictionaryLookup in onCreate"); + } + mUserDictionaryLookup = new UserDictionaryLookup(this); + } else if (DEBUG) { + Log.d(TAG, "mUserDictionaryLookup already created before onCreate"); + } + } + + @Override + public void onDestroy() { + if (DEBUG) { + Log.d(TAG, "Closing and dereferencing mUserDictionaryLookup in onDestroy"); + } + mUserDictionaryLookup.close(); + mUserDictionaryLookup = null; + super.onDestroy(); } public float getRecommendedThreshold() { @@ -175,21 +141,8 @@ public final class AndroidSpellCheckerService extends SpellCheckerService @Override 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); - } - } + final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); + mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary); } @Override @@ -221,24 +174,36 @@ public final class AndroidSpellCheckerService extends SpellCheckerService public boolean isValidWord(final Locale locale, final String word) { mSemaphore.acquireUninterruptibly(); try { + if (mUserDictionaryLookup.isValidWord(word, locale)) { + if (DEBUG) { + Log.d(TAG, "mUserDictionaryLookup.isValidWord(" + word + ")=true"); + } + return true; + } else { + if (DEBUG) { + Log.d(TAG, "mUserDictionaryLookup.isValidWord(" + word + ")=false"); + } + } DictionaryFacilitator dictionaryFacilitatorForLocale = - getDictionaryFacilitatorForLocaleLocked(locale); - return dictionaryFacilitatorForLocale.isValidWord(word, false /* igroreCase */); + mDictionaryFacilitatorCache.get(locale); + return dictionaryFacilitatorForLocale.isValidSpellingWord(word); } finally { mSemaphore.release(); } } - public SuggestionResults getSuggestionResults(final Locale locale, final WordComposer composer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo) { + public SuggestionResults getSuggestionResults(final Locale locale, + final ComposedData composedData, final NgramContext ngramContext, + @Nonnull final Keyboard keyboard) { 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(composedData, ngramContext, + keyboard, mSettingsValuesForSuggestion, + sessionId, SuggestedWords.INPUT_STYLE_TYPING); } finally { if (sessionId != null) { mSessionIdPool.add(sessionId); @@ -251,56 +216,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(); + mDictionaryFacilitatorCache.closeDictionaries(); } finally { mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); } @@ -334,7 +261,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(RichInputMethodSubtype.getRichInputMethodSubtype(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..da5c71738 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -29,19 +29,19 @@ 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.LocaleUtils; +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.StatsUtils; import com.android.inputmethod.latin.utils.SuggestionResults; import java.util.ArrayList; +import java.util.List; import java.util.Locale; public abstract class AndroidWordLevelSpellCheckerSession extends Session { @@ -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() { @@ -117,7 +115,8 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { @Override public void onCreate() { final String localeString = getLocale(); - mLocale = LocaleUtils.constructLocaleFromString(localeString); + mLocale = (null == localeString) ? null + : LocaleUtils.constructLocaleFromString(localeString); mScript = ScriptUtils.getScriptFromSpellCheckerLocale(mLocale); } @@ -223,12 +222,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); @@ -262,31 +260,28 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { final String text = inText.replaceAll( AndroidSpellCheckerService.APOSTROPHE, AndroidSpellCheckerService.SINGLE_QUOTE); final int capitalizeType = StringUtils.getCapitalizationType(text); - boolean isInDict = true; if (!mService.hasMainDictionaryForLocale(mLocale)) { return AndroidSpellCheckerService.getNotInDictEmptySuggestions( false /* reportAsTypo */); } final Keyboard keyboard = mService.getKeyboardForLocale(mLocale); + if (null == keyboard) { + Log.d(TAG, "No keyboard for locale: " + mLocale); + // If there is no keyboard for this locale, don't do any spell-checking. + return AndroidSpellCheckerService.getNotInDictEmptySuggestions( + false /* reportAsTypo */); + } final WordComposer composer = new WordComposer(); final int[] codePoints = StringUtils.toCodePointArray(text); final int[] coordinates; - final ProximityInfo proximityInfo; - if (null == keyboard) { - coordinates = CoordinateUtils.newCoordinateArray(codePoints.length, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); - proximityInfo = null; - } else { - coordinates = keyboard.getCoordinates(codePoints); - proximityInfo = keyboard.getProximityInfo(); - } + coordinates = keyboard.getCoordinates(codePoints); 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.getComposedDataSnapshot(), ngramContext, keyboard); final Result result = getResult(capitalizeType, mLocale, suggestionsLimit, mService.getRecommendedThreshold(), text, suggestionResults); - isInDict = isInDictForAnyCapitalization(text, capitalizeType); + final boolean isInDict = isInDictForAnyCapitalization(text, capitalizeType); if (DBG) { Log.i(TAG, "Spell checking results for " + text + " with suggestion limit " + suggestionsLimit); @@ -299,6 +294,15 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { } } } + // Handle word not in dictionary. + // This is called only once per unique word, so entering multiple + // instances of the same word does not result in more than one call + // to this method. + // Also, upon changing the orientation of the device, this is called + // again for every unique invalid word in the text box. + if (!isInDict) { + StatsUtils.onInvalidWordIdentification(text); + } final int flags = (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY @@ -308,26 +312,24 @@ 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 */); } } private static final class Result { public final String[] mSuggestions; public final boolean mHasRecommendedSuggestions; - public Result(final String[] gatheredSuggestions, - final boolean hasRecommendedSuggestions) { + public Result(final String[] gatheredSuggestions, final boolean hasRecommendedSuggestions) { mSuggestions = gatheredSuggestions; mHasRecommendedSuggestions = hasRecommendedSuggestions; } @@ -361,14 +363,15 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { StringUtils.removeDupes(suggestions); // This returns a String[], while toArray() returns an Object[] which cannot be cast // into a String[]. + final List<String> gatheredSuggestionsList = + suggestions.subList(0, Math.min(suggestions.size(), suggestionsLimit)); final String[] gatheredSuggestions = - suggestions.subList(0, Math.min(suggestions.size(), suggestionsLimit)) - .toArray(EMPTY_STRING_ARRAY); + gatheredSuggestionsList.toArray(new String[gatheredSuggestionsList.size()]); final int bestScore = suggestionResults.first().mScore; final String bestSuggestion = suggestions.get(0); final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( - originalText, bestSuggestion.toString(), bestScore); + originalText, bestSuggestion, bestScore); final boolean hasRecommendedSuggestions = (normalizedScore > recommendedThreshold); if (DBG) { Log.i(TAG, "Best suggestion : " + bestSuggestion + ", score " + bestScore); @@ -387,8 +390,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { * That's what the following method does. */ @Override - public SuggestionsInfo onGetSuggestions(final TextInfo textInfo, - final int suggestionsLimit) { + public SuggestionsInfo onGetSuggestions(final TextInfo textInfo, final int suggestionsLimit) { long ident = Binder.clearCallingIdentity(); try { return onGetSuggestionsInternal(textInfo, suggestionsLimit); 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/spellcheck/UserDictionaryLookup.java b/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java new file mode 100644 index 000000000..f2491f478 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/spellcheck/UserDictionaryLookup.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.spellcheck; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.provider.UserDictionary; +import android.util.Log; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.common.LocaleUtils; +import com.android.inputmethod.latin.utils.ExecutorUtils; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * UserDictionaryLookup provides the ability to lookup into the system-wide "Personal dictionary". + * + * Note, that the initial dictionary loading happens asynchronously so it is possible (hopefully + * rarely) that isValidWord is called before the initial load has started. + * + * The caller should explicitly call close() when the object is no longer needed, in order to + * release any resources and references to this object. A service should create this object in + * onCreate and close() it in onDestroy. + */ +public class UserDictionaryLookup implements Closeable { + private static final String TAG = UserDictionaryLookup.class.getSimpleName(); + + /** + * This guards the execution of any Log.d() logging, so that if false, they are not even + */ + private static final boolean DEBUG = false; + + /** + * To avoid loading too many dictionary entries in memory, we cap them at this number. If + * that number is exceeded, the lowest-frequency items will be dropped. Note, there is no + * explicit cap on the number of locales in every entry. + */ + private static final int MAX_NUM_ENTRIES = 1000; + + /** + * The delay (in milliseconds) to impose on reloads. Previously scheduled reloads will be + * cancelled if a new reload is scheduled before the delay expires. Thus, only the last + * reload in the series of frequent reloads will execute. + * + * Note, this value should be low enough to allow the "Add to dictionary" feature in the + * TextView correction (red underline) drop-down menu to work properly in the following case: + * + * 1. User types OOV (out-of-vocabulary) word. + * 2. The OOV is red-underlined. + * 3. User selects "Add to dictionary". The red underline disappears while the OOV is + * in a composing span. + * 4. The user taps space. The red underline should NOT reappear. If this value is very + * high and the user performs the space tap fast enough, the red underline may reappear. + */ + @UsedForTesting + static final int RELOAD_DELAY_MS = 200; + + private final ContentResolver mResolver; + + /** + * Runnable that calls loadUserDictionary(). + */ + private class UserDictionaryLoader implements Runnable { + @Override + public void run() { + if (DEBUG) { + Log.d(TAG, "Executing (re)load"); + } + loadUserDictionary(); + } + } + private final UserDictionaryLoader mLoader = new UserDictionaryLoader(); + + /** + * Content observer for UserDictionary changes. It has the following properties: + * 1. It spawns off a UserDictionary reload in another thread, after some delay. + * 2. It cancels previously scheduled reloads, and only executes the latest. + * 3. It may be called multiple times quickly in succession (and is in fact called so + * when UserDictionary is edited through its settings UI, when sometimes multiple + * notifications are sent for the edited entry, but also for the entire UserDictionary). + */ + private class UserDictionaryContentObserver extends ContentObserver { + public UserDictionaryContentObserver() { + super(null); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + // Support pre-API16 platforms. + @Override + public void onChange(boolean selfChange) { + onChange(selfChange, null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (DEBUG) { + Log.d(TAG, "Received content observer onChange notification for URI: " + uri); + } + // Cancel (but don't interrupt) any pending reloads (except the initial load). + if (mReloadFuture != null && !mReloadFuture.isCancelled() && + !mReloadFuture.isDone()) { + // Note, that if already cancelled or done, this will do nothing. + boolean isCancelled = mReloadFuture.cancel(false); + if (DEBUG) { + if (isCancelled) { + Log.d(TAG, "Successfully canceled previous reload request"); + } else { + Log.d(TAG, "Unable to cancel previous reload request"); + } + } + } + + if (DEBUG) { + Log.d(TAG, "Scheduling reload in " + RELOAD_DELAY_MS + " ms"); + } + + // Schedule a new reload after RELOAD_DELAY_MS. + mReloadFuture = ExecutorUtils.getBackgroundExecutor(ExecutorUtils.SPELLING) + .schedule(mLoader, RELOAD_DELAY_MS, TimeUnit.MILLISECONDS); + } + } + private final ContentObserver mObserver = new UserDictionaryContentObserver(); + + /** + * Indicates that a load is in progress, so no need for another. + */ + private AtomicBoolean mIsLoading = new AtomicBoolean(false); + + /** + * Indicates that this lookup object has been close()d. + */ + private AtomicBoolean mIsClosed = new AtomicBoolean(false); + + /** + * We store a map from a dictionary word to the set of locales it belongs + * in. We then iterate over the set of locales to find a match using + * LocaleUtils. + */ + private volatile HashMap<String, ArrayList<Locale>> mDictWords; + + /** + * The last-scheduled reload future. Saved in order to cancel a pending reload if a new one + * is coming. + */ + private volatile ScheduledFuture<?> mReloadFuture; + + /** + * @param context the context from which to obtain content resolver + */ + public UserDictionaryLookup(Context context) { + if (DEBUG) { + Log.d(TAG, "UserDictionaryLookup constructor with context: " + context); + } + + // Obtain a content resolver. + mResolver = context.getContentResolver(); + + // Schedule the initial load to run immediately. It's possible that the first call to + // isValidWord occurs before the dictionary has actually loaded, so it should not + // assume that the dictionary has been loaded. + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.SPELLING).execute(mLoader); + + // Register the observer to be notified on changes to the UserDictionary and all individual + // items. + // + // If the user is interacting with the UserDictionary settings UI, or with the + // "Add to dictionary" drop-down option, duplicate notifications will be sent for the same + // edit: if a new entry is added, there is a notification for the entry itself, and + // separately for the entire dictionary. However, when used programmatically, + // only notifications for the specific edits are sent. Thus, the observer is registered to + // receive every possible notification, and instead has throttling logic to avoid doing too + // many reloads. + mResolver.registerContentObserver( + UserDictionary.Words.CONTENT_URI, true /* notifyForDescendents */, mObserver); + } + + /** + * To be called by the garbage collector in the off chance that the service did not clean up + * properly. Do not rely on this getting called, and make sure close() is called explicitly. + */ + @Override + public void finalize() throws Throwable { + try { + if (DEBUG) { + Log.d(TAG, "Finalize called, calling close()"); + } + close(); + } finally { + super.finalize(); + } + } + + /** + * Cleans up UserDictionaryLookup: shuts down any extra threads and unregisters the observer. + * + * It is safe, but not advised to call this multiple times, and isValidWord would continue to + * work, but no data will be reloaded any longer. + */ + @Override + public void close() { + if (DEBUG) { + Log.d(TAG, "Close called (no pun intended), cleaning up executor and observer"); + } + if (mIsClosed.compareAndSet(false, true)) { + // Unregister the content observer. + mResolver.unregisterContentObserver(mObserver); + } + } + + /** + * Returns true if the initial load has been performed. + * + * @return true if the initial load is successful + */ + @UsedForTesting + boolean isLoaded() { + return mDictWords != null; + } + + /** + * Determines if the given word is a valid word in the given locale based on the UserDictionary. + * It tries hard to find a match: for example, casing is ignored and if the word is present in a + * more general locale (e.g. en or all locales), and isValidWord is asking for a more specific + * locale (e.g. en_US), it will be considered a match. + * + * @param word the word to match + * @param locale the locale in which to match the word + * @return true iff the word has been matched for this locale in the UserDictionary. + */ + public boolean isValidWord( + final String word, final Locale locale) { + if (!isLoaded()) { + // This is a corner case in the event the initial load of UserDictionary has not + // been loaded. In that case, we assume the word is not a valid word in + // UserDictionary. + if (DEBUG) { + Log.d(TAG, "isValidWord invoked, but initial load not complete"); + } + return false; + } + + // Atomically obtain the current copy of mDictWords; + final HashMap<String, ArrayList<Locale>> dictWords = mDictWords; + + if (DEBUG) { + Log.d(TAG, "isValidWord invoked for word [" + word + + "] in locale " + locale); + } + // Lowercase the word using the given locale. Note, that dictionary + // words are lowercased using their locale, and theoretically the + // lowercasing between two matching locales may differ. For simplicity + // we ignore that possibility. + final String lowercased = word.toLowerCase(locale); + final ArrayList<Locale> dictLocales = dictWords.get(lowercased); + if (null == dictLocales) { + if (DEBUG) { + Log.d(TAG, "isValidWord=false, since there is no entry for " + + "lowercased word [" + lowercased + "]"); + } + return false; + } else { + if (DEBUG) { + Log.d(TAG, "isValidWord found an entry for lowercased word [" + lowercased + + "]; examining locales"); + } + // Iterate over the locales this word is in. + for (final Locale dictLocale : dictLocales) { + final int matchLevel = LocaleUtils.getMatchLevel(dictLocale.toString(), + locale.toString()); + if (DEBUG) { + Log.d(TAG, "matchLevel for dictLocale=" + dictLocale + ", locale=" + + locale + " is " + matchLevel); + } + if (LocaleUtils.isMatch(matchLevel)) { + if (DEBUG) { + Log.d(TAG, "isValidWord=true, since matchLevel " + matchLevel + + " is a match"); + } + return true; + } + if (DEBUG) { + Log.d(TAG, "matchLevel " + matchLevel + " is not a match"); + } + } + if (DEBUG) { + Log.d(TAG, "isValidWord=false, since none of the locales matched"); + } + return false; + } + } + + /** + * Loads the UserDictionary in the current thread. + * + * Only one reload can happen at a time. If already running, will exit quickly. + */ + private void loadUserDictionary() { + // Bail out if already in the process of loading. + if (!mIsLoading.compareAndSet(false, true)) { + if (DEBUG) { + Log.d(TAG, "Already in the process of loading UserDictionary, skipping"); + } + return; + } + if (DEBUG) { + Log.d(TAG, "Loading UserDictionary"); + } + HashMap<String, ArrayList<Locale>> dictWords = new HashMap<>(); + // Load the UserDictionary. Request that items be returned in the default sort order + // for UserDictionary, which is by frequency. + Cursor cursor = mResolver.query(UserDictionary.Words.CONTENT_URI, + null, null, null, UserDictionary.Words.DEFAULT_SORT_ORDER); + if (null == cursor || cursor.getCount() < 1) { + if (DEBUG) { + Log.d(TAG, "No entries found in UserDictionary"); + } + } else { + // Iterate over the entries in the UserDictionary. Note, that iteration is in + // descending frequency by default. + while (dictWords.size() < MAX_NUM_ENTRIES && cursor.moveToNext()) { + // If there is no column for locale, skip this entry. An empty + // locale on the other hand will not be skipped. + final int dictLocaleIndex = cursor.getColumnIndex( + UserDictionary.Words.LOCALE); + if (dictLocaleIndex < 0) { + if (DEBUG) { + Log.d(TAG, "Encountered UserDictionary entry " + + "without LOCALE, skipping"); + } + continue; + } + // If there is no column for word, skip this entry. + final int dictWordIndex = cursor.getColumnIndex( + UserDictionary.Words.WORD); + if (dictWordIndex < 0) { + if (DEBUG) { + Log.d(TAG, "Encountered UserDictionary entry without " + + "WORD, skipping"); + } + continue; + } + // If the word is null, skip this entry. + final String rawDictWord = cursor.getString(dictWordIndex); + if (null == rawDictWord) { + if (DEBUG) { + Log.d(TAG, "Encountered null word"); + } + continue; + } + // If the locale is null, that's interpreted to mean all locales. Note, the special + // zz locale for an Alphabet (QWERTY) layout will not match any actual language. + String localeString = cursor.getString(dictLocaleIndex); + if (null == localeString) { + if (DEBUG) { + Log.d(TAG, "Encountered null locale for word [" + + rawDictWord + "], assuming all locales"); + } + // For purposes of LocaleUtils, an empty locale matches + // everything. + localeString = ""; + } + final Locale dictLocale = LocaleUtils.constructLocaleFromString( + localeString); + // Lowercase the word before storing it. + final String dictWord = rawDictWord.toLowerCase(dictLocale); + if (DEBUG) { + Log.d(TAG, "Incorporating UserDictionary word [" + dictWord + + "] for locale " + dictLocale); + } + // Check if there is an existing entry for this word. + ArrayList<Locale> dictLocales = dictWords.get(dictWord); + if (null == dictLocales) { + // If there is no entry for this word, create one. + if (DEBUG) { + Log.d(TAG, "Word [" + dictWord + + "] not seen for other locales, creating new entry"); + } + dictLocales = new ArrayList<>(); + dictWords.put(dictWord, dictLocales); + } + // Append the locale to the list of locales this word is in. + dictLocales.add(dictLocale); + } + } + + // Atomically replace the copy of mDictWords. + mDictWords = dictWords; + + // Allow other calls to loadUserDictionary to execute now. + mIsLoading.set(false); + } +} 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..d8926ffba 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java @@ -28,7 +28,6 @@ import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.support.v4.view.ViewCompat; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; @@ -50,16 +49,16 @@ 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; @@ -94,8 +93,6 @@ final class SuggestionStripLayoutHelper { private final int mTypedWordPositionWhenAutocorrect; private final Drawable mMoreSuggestionsHint; private static final String MORE_SUGGESTIONS_HINT = "\u2026"; - private static final String LEFTWARDS_ARROW = "\u2190"; - private static final String RIGHTWARDS_ARROW = "\u2192"; private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); @@ -213,15 +210,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 +234,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 +315,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); } @@ -358,25 +342,30 @@ final class SuggestionStripLayoutHelper { * @param placerView the view where the debug info will be placed. * @return the start index of more suggestions. */ - public int layoutAndReturnStartIndexOfMoreSuggestions(final SuggestedWords suggestedWords, - final ViewGroup stripView, final ViewGroup placerView) { + public int layoutAndReturnStartIndexOfMoreSuggestions( + final Context context, + final SuggestedWords suggestedWords, + final ViewGroup stripView, + final ViewGroup placerView) { if (suggestedWords.isPunctuationSuggestions()) { return layoutPunctuationsAndReturnStartIndexOfMoreSuggestions( (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); - layoutWord(mCenterPositionInStrip, stripWidth - mPadding); + mMoreSuggestionsAvailable = (wordCountToShow > countInStrip); + layoutWord(context, mCenterPositionInStrip, stripWidth - mPadding); stripView.addView(centerWordView); setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT); if (SuggestionStripView.DBG) { @@ -387,7 +376,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) { @@ -398,7 +388,7 @@ final class SuggestionStripLayoutHelper { } final int width = getSuggestionWidth(positionInStrip, stripWidth); - final TextView wordView = layoutWord(positionInStrip, width); + final TextView wordView = layoutWord(context, positionInStrip, width); stripView.addView(wordView); setLayoutWeight(wordView, getSuggestionWeight(positionInStrip), ViewGroup.LayoutParams.MATCH_PARENT); @@ -427,7 +417,7 @@ final class SuggestionStripLayoutHelper { * @param width the maximum width for layout in pixels. * @return the {@link TextView} containing the suggested word appropriately formatted. */ - private TextView layoutWord(final int positionInStrip, final int width) { + private TextView layoutWord(final Context context, final int positionInStrip, final int width) { final TextView wordView = mWordViews.get(positionInStrip); final CharSequence word = wordView.getText(); if (positionInStrip == mCenterPositionInStrip && mMoreSuggestionsAvailable) { @@ -441,11 +431,15 @@ 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()); + wordView.setContentDescription( + TextUtils.isEmpty(word) + ? context.getResources().getString(R.string.spoken_empty_suggestion) + : word.toString()); + 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,55 +542,6 @@ final class SuggestionStripLayoutHelper { return countInStrip; } - public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip) { - final boolean shouldShowUiToAcceptTypedWord = Settings.getInstance().getCurrent() - .mShouldShowUiToAcceptTypedWord; - final int stripWidth = addToDictionaryStrip.getWidth(); - final int width = shouldShowUiToAcceptTypedWord ? stripWidth - : stripWidth - mDividerWidth - mPadding * 2; - - 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 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; - wordView.setVisibility(wordVisibility); - addToDictionaryStrip.findViewById(R.id.word_to_save_divider).setVisibility(wordVisibility); - - final Resources res = addToDictionaryStrip.getResources(); - final CharSequence hintText; - final int hintWidth; - 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 { - final boolean isRtlLanguage = (ViewCompat.getLayoutDirection(addToDictionaryStrip) - == ViewCompat.LAYOUT_DIRECTION_RTL); - final String arrow = isRtlLanguage ? RIGHTWARDS_ARROW : LEFTWARDS_ARROW; - final boolean isRtlSystem = SubtypeLocaleUtils.isRtlLanguage( - res.getConfiguration().locale); - final CharSequence hint = res.getText(R.string.hint_add_to_dictionary); - hintText = (isRtlLanguage == isRtlSystem) ? (arrow + hint) : (hint + arrow); - hintWidth = width - wordWidth; - hintWeight = 1.0f - mCenterSuggestionWeight; - hintView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); - } - hintView.setTextColor(mColorAutoCorrect); - final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint()); - hintView.setText(hintText); - hintView.setTextScaleX(hintScaleX); - setLayoutWeight(hintView, hintWeight, ViewGroup.LayoutParams.MATCH_PARENT); - } - public void layoutImportantNotice(final View importantNoticeStrip, final String importantNoticeTitle) { final TextView titleView = (TextView)importantNoticeStrip.findViewById( @@ -604,8 +549,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 +564,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 +586,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 int getTextWidth(final CharSequence text, final TextPaint paint) { + 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(@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..7dd0f03df 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; @@ -58,7 +58,6 @@ import java.util.ArrayList; public final class SuggestionStripView extends RelativeLayout implements OnClickListener, OnLongClickListener { public interface Listener { - public void addWordToUserDictionary(String word); public void showImportantNoticeContents(); public void pickSuggestionManually(SuggestedWordInfo word); public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat); @@ -69,7 +68,6 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick private final ViewGroup mSuggestionsStrip; private final ImageButton mVoiceKey; - private final ViewGroup mAddToDictionaryStrip; private final View mImportantNoticeStrip; MainKeyboardView mMainKeyboardView; @@ -82,7 +80,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; @@ -91,15 +89,12 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick private static class StripVisibilityGroup { private final View mSuggestionStripView; private final View mSuggestionsStrip; - private final View mAddToDictionaryStrip; private final View mImportantNoticeStrip; public StripVisibilityGroup(final View suggestionStripView, - final ViewGroup suggestionsStrip, final ViewGroup addToDictionaryStrip, - final View importantNoticeStrip) { + final ViewGroup suggestionsStrip, final View importantNoticeStrip) { mSuggestionStripView = suggestionStripView; mSuggestionsStrip = suggestionsStrip; - mAddToDictionaryStrip = addToDictionaryStrip; mImportantNoticeStrip = importantNoticeStrip; showSuggestionsStrip(); } @@ -109,30 +104,21 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick : ViewCompat.LAYOUT_DIRECTION_LTR; ViewCompat.setLayoutDirection(mSuggestionStripView, layoutDirection); ViewCompat.setLayoutDirection(mSuggestionsStrip, layoutDirection); - ViewCompat.setLayoutDirection(mAddToDictionaryStrip, layoutDirection); ViewCompat.setLayoutDirection(mImportantNoticeStrip, layoutDirection); } public void showSuggestionsStrip() { mSuggestionsStrip.setVisibility(VISIBLE); - mAddToDictionaryStrip.setVisibility(INVISIBLE); - mImportantNoticeStrip.setVisibility(INVISIBLE); - } - - public void showAddToDictionaryStrip() { - mSuggestionsStrip.setVisibility(INVISIBLE); - mAddToDictionaryStrip.setVisibility(VISIBLE); mImportantNoticeStrip.setVisibility(INVISIBLE); } public void showImportantNoticeStrip() { mSuggestionsStrip.setVisibility(INVISIBLE); - mAddToDictionaryStrip.setVisibility(INVISIBLE); mImportantNoticeStrip.setVisibility(VISIBLE); } - public boolean isShowingAddToDictionaryStrip() { - return mAddToDictionaryStrip.getVisibility() == VISIBLE; + public boolean isShowingImportantNoticeStrip() { + return mImportantNoticeStrip.getVisibility() == VISIBLE; } } @@ -154,13 +140,13 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip); mVoiceKey = (ImageButton)findViewById(R.id.suggestions_strip_voice_key); - mAddToDictionaryStrip = (ViewGroup)findViewById(R.id.add_to_dictionary_strip); mImportantNoticeStrip = findViewById(R.id.important_notice_strip); mStripVisibilityGroup = new StripVisibilityGroup(this, mSuggestionsStrip, - mAddToDictionaryStrip, mImportantNoticeStrip); + mImportantNoticeStrip); for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) { final TextView word = new TextView(context, null, R.attr.suggestionWordStyle); + word.setContentDescription(getResources().getString(R.string.spoken_empty_suggestion)); word.setOnClickListener(this); word.setOnLongClickListener(this); mWordViews.add(word); @@ -215,7 +201,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mStripVisibilityGroup.setLayoutDirection(isRtlLanguage); mSuggestedWords = suggestedWords; mStartIndexOfMoreSuggestions = mLayoutHelper.layoutAndReturnStartIndexOfMoreSuggestions( - mSuggestedWords, mSuggestionsStrip, this); + getContext(), mSuggestedWords, mSuggestionsStrip, this); mStripVisibilityGroup.showSuggestionsStrip(); } @@ -223,32 +209,12 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mLayoutHelper.setMoreSuggestionsHeight(remainingHeight); } - public boolean isShowingAddToDictionaryHint() { - return mStripVisibilityGroup.isShowingAddToDictionaryStrip(); - } - - public void showAddToDictionaryHint(final String word) { - mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip); - // {@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); - mAddToDictionaryStrip.setOnClickListener(this); - mStripVisibilityGroup.showAddToDictionaryStrip(); - } - - public boolean dismissAddToDictionaryHint() { - if (isShowingAddToDictionaryHint()) { - clear(); - return true; - } - return false; - } - // This method checks if we should show the important notice (checks on permanent storage if // it has been shown once already or not, and if in the setup wizard). If applicable, it shows // the notice. In all cases, it returns true if it was shown, false otherwise. public boolean maybeShowImportantNoticeTitle() { - if (!ImportantNoticeUtils.shouldShowImportantNotice(getContext())) { + final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent(); + if (!ImportantNoticeUtils.shouldShowImportantNotice(getContext(), currentSettingsValues)) { return false; } if (getWidth() <= 0) { @@ -340,12 +306,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 +353,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 +383,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 +396,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(); @@ -484,15 +456,8 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick false /* isKeyRepeat */); return; } - final Object tag = view.getTag(); - // {@link String} tag is set at {@link #showAddToDictionaryHint(String,CharSequence)}. - if (tag instanceof String) { - final String wordToSave = (String)tag; - mListener.addWordToUserDictionary(wordToSave); - clear(); - return; - } + final Object tag = view.getTag(); // {@link Integer} tag is set at // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup} diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java index 52708455e..68f417e84 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java @@ -22,9 +22,6 @@ 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 boolean isShowingAddToDictionaryHint(); - public void dismissAddToDictionaryHint(); public void setNeutralSuggestionStrip(); public void showSuggestionStrip(final SuggestedWords suggestedWords); } diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java index eda81940f..cb615f3af 100644 --- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java +++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java @@ -26,14 +26,15 @@ import android.text.TextUtils; import android.view.View; import android.widget.EditText; -import com.android.inputmethod.compat.UserDictionaryCompatUtils; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.utils.LocaleUtils; +import com.android.inputmethod.latin.common.LocaleUtils; import java.util.ArrayList; import java.util.Locale; import java.util.TreeSet; +import javax.annotation.Nullable; + // Caveat: This class is basically taken from // packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java // in order to deal with some devices that have issues with the user dictionary handling @@ -45,10 +46,7 @@ import java.util.TreeSet; public class UserDictionaryAddWordContents { public static final String EXTRA_MODE = "mode"; public static final String EXTRA_WORD = "word"; - public static final String EXTRA_SHORTCUT = "shortcut"; public static final String EXTRA_LOCALE = "locale"; - public static final String EXTRA_ORIGINAL_WORD = "originalWord"; - public static final String EXTRA_ORIGINAL_SHORTCUT = "originalShortcut"; public static final int MODE_EDIT = 0; public static final int MODE_INSERT = 1; @@ -61,20 +59,12 @@ public class UserDictionaryAddWordContents { private final int mMode; // Either MODE_EDIT or MODE_INSERT private final EditText mWordEditText; - private final EditText mShortcutEditText; private String mLocale; private final String mOldWord; - private final String mOldShortcut; private String mSavedWord; - private String mSavedShortcut; /* package */ UserDictionaryAddWordContents(final View view, final Bundle args) { mWordEditText = (EditText)view.findViewById(R.id.user_dictionary_add_word_text); - mShortcutEditText = (EditText)view.findViewById(R.id.user_dictionary_add_shortcut); - if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { - mShortcutEditText.setVisibility(View.GONE); - view.findViewById(R.id.user_dictionary_add_shortcut_label).setVisibility(View.GONE); - } final String word = args.getString(EXTRA_WORD); if (null != word) { mWordEditText.setText(word); @@ -82,17 +72,6 @@ public class UserDictionaryAddWordContents { // it's too long to be edited. mWordEditText.setSelection(mWordEditText.getText().length()); } - final String shortcut; - if (UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { - shortcut = args.getString(EXTRA_SHORTCUT); - if (null != shortcut && null != mShortcutEditText) { - mShortcutEditText.setText(shortcut); - } - mOldShortcut = args.getString(EXTRA_SHORTCUT); - } else { - shortcut = null; - mOldShortcut = null; - } mMode = args.getInt(EXTRA_MODE); // default return value for #getInt() is 0 = MODE_EDIT mOldWord = args.getString(EXTRA_WORD); updateLocale(args.getString(EXTRA_LOCALE)); @@ -101,10 +80,8 @@ public class UserDictionaryAddWordContents { /* package */ UserDictionaryAddWordContents(final View view, final UserDictionaryAddWordContents oldInstanceToBeEdited) { mWordEditText = (EditText)view.findViewById(R.id.user_dictionary_add_word_text); - mShortcutEditText = (EditText)view.findViewById(R.id.user_dictionary_add_shortcut); mMode = MODE_EDIT; mOldWord = oldInstanceToBeEdited.mSavedWord; - mOldShortcut = oldInstanceToBeEdited.mSavedShortcut; updateLocale(mLocale); } @@ -116,13 +93,6 @@ public class UserDictionaryAddWordContents { /* package */ void saveStateIntoBundle(final Bundle outState) { outState.putString(EXTRA_WORD, mWordEditText.getText().toString()); - outState.putString(EXTRA_ORIGINAL_WORD, mOldWord); - if (null != mShortcutEditText) { - outState.putString(EXTRA_SHORTCUT, mShortcutEditText.getText().toString()); - } - if (null != mOldShortcut) { - outState.putString(EXTRA_ORIGINAL_SHORTCUT, mOldShortcut); - } outState.putString(EXTRA_LOCALE, mLocale); } @@ -130,7 +100,7 @@ public class UserDictionaryAddWordContents { if (MODE_EDIT == mMode && !TextUtils.isEmpty(mOldWord)) { // Mode edit: remove the old entry. final ContentResolver resolver = context.getContentResolver(); - UserDictionarySettings.deleteWord(mOldWord, mOldShortcut, resolver); + UserDictionarySettings.deleteWord(mOldWord, resolver); } // If we are in add mode, nothing was added, so we don't need to do anything. } @@ -141,50 +111,31 @@ public class UserDictionaryAddWordContents { final ContentResolver resolver = context.getContentResolver(); if (MODE_EDIT == mMode && !TextUtils.isEmpty(mOldWord)) { // Mode edit: remove the old entry. - UserDictionarySettings.deleteWord(mOldWord, mOldShortcut, resolver); + UserDictionarySettings.deleteWord(mOldWord, resolver); } final String newWord = mWordEditText.getText().toString(); - final String newShortcut; - if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { - newShortcut = null; - } else if (null == mShortcutEditText) { - newShortcut = null; - } else { - final String tmpShortcut = mShortcutEditText.getText().toString(); - if (TextUtils.isEmpty(tmpShortcut)) { - newShortcut = null; - } else { - newShortcut = tmpShortcut; - } - } if (TextUtils.isEmpty(newWord)) { // If the word is somehow empty, don't insert it. return CODE_CANCEL; } mSavedWord = newWord; - mSavedShortcut = newShortcut; - // If there is no shortcut, and the word already exists in the database, then we - // should not insert, because either A. the word exists with no shortcut, in which - // case the exact same thing we want to insert is already there, or B. the word - // exists with at least one shortcut, in which case it has priority on our word. - if (TextUtils.isEmpty(newShortcut) && hasWord(newWord, context)) { + // If the word already exists in the database, then we should not insert. + if (hasWord(newWord, context)) { return CODE_ALREADY_PRESENT; } - // Disallow duplicates. If the same word with no shortcut is defined, remove it; if - // the same word with the same shortcut is defined, remove it; but we don't mind if - // there is the same word with a different, non-empty shortcut. - UserDictionarySettings.deleteWord(newWord, null, resolver); - if (!TextUtils.isEmpty(newShortcut)) { - // If newShortcut is empty we just deleted this, no need to do it again - UserDictionarySettings.deleteWord(newWord, newShortcut, resolver); - } + // Disallow duplicates. If the same word is defined, remove it. + UserDictionarySettings.deleteWord(newWord, resolver); // In this class we use the empty string to represent 'all locales' and mLocale cannot // be null. However the addWord method takes null to mean 'all locales'. - UserDictionaryCompatUtils.addWord(context, newWord.toString(), - FREQUENCY_FOR_USER_DICTIONARY_ADDS, newShortcut, TextUtils.isEmpty(mLocale) ? - null : LocaleUtils.constructLocaleFromString(mLocale)); + final Locale locale = TextUtils.isEmpty(mLocale) ? + null : LocaleUtils.constructLocaleFromString(mLocale); + final Locale currentLocale = context.getResources().getConfiguration().locale; + final boolean useCurrentLocale = currentLocale.equals(locale); + UserDictionary.Words.addWord(context, newWord.toString(), + FREQUENCY_FOR_USER_DICTIONARY_ADDS, null /* shortcut */, + useCurrentLocale ? Locale.getDefault() : null); return CODE_WORD_ADDED; } @@ -218,8 +169,8 @@ public class UserDictionaryAddWordContents { public static class LocaleRenderer { private final String mLocaleString; private final String mDescription; - // LocaleString may NOT be null. - public LocaleRenderer(final Context context, final String localeString) { + + public LocaleRenderer(final Context context, @Nullable final String localeString) { mLocaleString = localeString; if (null == localeString) { mDescription = context.getString(R.string.user_dict_settings_more_languages); diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java deleted file mode 100644 index 163443036..000000000 --- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2013 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.userdictionary; - -import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.userdictionary.UserDictionaryAddWordContents.LocaleRenderer; -import com.android.inputmethod.latin.userdictionary.UserDictionaryLocalePicker.LocationChangedListener; - -import android.app.Fragment; -import android.os.Bundle; -import android.preference.PreferenceActivity; -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.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Spinner; - -import java.util.ArrayList; -import java.util.Locale; - -// Caveat: This class is basically taken from -// packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryAddWordFragment.java -// in order to deal with some devices that have issues with the user dictionary handling - -/** - * Fragment to add a word/shortcut to the user dictionary. - * - * As opposed to the UserDictionaryActivity, this is only invoked within Settings - * from the UserDictionarySettings. - */ -public class UserDictionaryAddWordFragment extends Fragment - implements AdapterView.OnItemSelectedListener, LocationChangedListener { - - private static final int OPTIONS_MENU_ADD = Menu.FIRST; - private static final int OPTIONS_MENU_DELETE = Menu.FIRST + 1; - - private UserDictionaryAddWordContents mContents; - private View mRootView; - private boolean mIsDeleting = false; - - @Override - public void onActivityCreated(final Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(true); - getActivity().getActionBar().setTitle(R.string.edit_personal_dictionary); - // Keep the instance so that we remember mContents when configuration changes (eg rotation) - setRetainInstance(true); - } - - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedState) { - mRootView = inflater.inflate(R.layout.user_dictionary_add_word_fullscreen, null); - mIsDeleting = false; - // If we have a non-null mContents object, it's the old value before a configuration - // change (eg rotation) so we need to use its values. Otherwise, read from the arguments. - if (null == mContents) { - mContents = new UserDictionaryAddWordContents(mRootView, getArguments()); - } else { - // We create a new mContents object to account for the new situation : a word has - // been added to the user dictionary when we started rotating, and we are now editing - // it. That means in particular if the word undergoes any change, the old version should - // be updated, so the mContents object needs to switch to EDIT mode if it was in - // INSERT mode. - mContents = new UserDictionaryAddWordContents(mRootView, - mContents /* oldInstanceToBeEdited */); - } - getActivity().getActionBar().setSubtitle(UserDictionarySettingsUtils.getLocaleDisplayName( - getActivity(), mContents.getCurrentUserDictionaryLocale())); - return mRootView; - } - - @Override - public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { - final MenuItem actionItemAdd = menu.add(0, OPTIONS_MENU_ADD, 0, - R.string.user_dict_settings_add_menu_title).setIcon(R.drawable.ic_menu_add); - actionItemAdd.setShowAsAction( - MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - final MenuItem actionItemDelete = menu.add(0, OPTIONS_MENU_DELETE, 0, - R.string.user_dict_settings_delete).setIcon(android.R.drawable.ic_menu_delete); - actionItemDelete.setShowAsAction( - MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - } - - /** - * Callback for the framework when a menu option is pressed. - * - * @param item the item that was pressed - * @return false to allow normal menu processing to proceed, true to consume it here - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == OPTIONS_MENU_ADD) { - // added the entry in "onPause" - getActivity().onBackPressed(); - return true; - } - if (item.getItemId() == OPTIONS_MENU_DELETE) { - mContents.delete(getActivity()); - mIsDeleting = true; - getActivity().onBackPressed(); - return true; - } - return false; - } - - @Override - public void onResume() { - super.onResume(); - // We are being shown: display the word - updateSpinner(); - } - - private void updateSpinner() { - final ArrayList<LocaleRenderer> localesList = mContents.getLocalesList(getActivity()); - - final Spinner localeSpinner = - (Spinner)mRootView.findViewById(R.id.user_dictionary_add_locale); - final ArrayAdapter<LocaleRenderer> adapter = new ArrayAdapter<>( - getActivity(), android.R.layout.simple_spinner_item, localesList); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - localeSpinner.setAdapter(adapter); - localeSpinner.setOnItemSelectedListener(this); - } - - @Override - public void onPause() { - super.onPause(); - // We are being hidden: commit changes to the user dictionary, unless we were deleting it - if (!mIsDeleting) { - mContents.apply(getActivity(), null); - } - } - - @Override - public void onItemSelected(final AdapterView<?> parent, final View view, final int pos, - final long id) { - final LocaleRenderer locale = (LocaleRenderer)parent.getItemAtPosition(pos); - if (locale.isMoreLanguages()) { - PreferenceActivity preferenceActivity = (PreferenceActivity)getActivity(); - preferenceActivity.startPreferenceFragment(new UserDictionaryLocalePicker(), true); - } else { - mContents.updateLocale(locale.getLocaleString()); - } - } - - @Override - public void onNothingSelected(final AdapterView<?> parent) { - // I'm not sure we can come here, but if we do, that's the right thing to do. - final Bundle args = getArguments(); - mContents.updateLocale(args.getString(UserDictionaryAddWordContents.EXTRA_LOCALE)); - } - - // Called by the locale picker - @Override - public void onLocaleSelected(final Locale locale) { - mContents.updateLocale(locale.toString()); - getActivity().onBackPressed(); - } -} diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java index 624783a70..57347ce8c 100644 --- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java +++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java @@ -20,6 +20,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.database.Cursor; +import android.os.Build; import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; @@ -31,12 +32,14 @@ import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.utils.LocaleUtils; +import com.android.inputmethod.latin.common.LocaleUtils; import java.util.List; import java.util.Locale; import java.util.TreeSet; +import javax.annotation.Nullable; + // Caveat: This class is basically taken from // packages/apps/Settings/src/com/android/settings/inputmethod/UserDictionaryList.java // in order to deal with some devices that have issues with the user dictionary handling @@ -47,12 +50,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); @@ -72,7 +75,7 @@ public class UserDictionaryList extends PreferenceFragment { } finally { cursor.close(); } - if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { // For ICS, we need to show "For all languages" in case that the keyboard locale // is different from the system locale localeSet.add(""); @@ -108,7 +111,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,31 +124,33 @@ 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)); } } } /** * Create a single User Dictionary Preference object, with its parameters set. - * @param locale The locale for which this user dictionary is for. + * @param localeString The locale for which this user dictionary is for. * @return The corresponding preference. */ - protected Preference createUserDictionaryPreference(String locale, Activity activity) { + protected Preference createUserDictionaryPreference(@Nullable final String localeString) { final Preference newPref = new Preference(getActivity()); final Intent intent = new Intent(USER_DICTIONARY_SETTINGS_INTENT_ACTION); - if (null == locale) { + if (null == localeString) { newPref.setTitle(Locale.getDefault().getDisplayName()); } else { - if ("".equals(locale)) + if (localeString.isEmpty()) { newPref.setTitle(getString(R.string.user_dict_settings_all_languages)); - else - newPref.setTitle(LocaleUtils.constructLocaleFromString(locale).getDisplayName()); - intent.putExtra("locale", locale); - newPref.getExtras().putString("locale", locale); + } else { + newPref.setTitle( + LocaleUtils.constructLocaleFromString(localeString).getDisplayName()); + } + intent.putExtra("locale", localeString); + newPref.getExtras().putString("locale", localeString); } newPref.setIntent(intent); newPref.setFragment(UserDictionarySettings.class.getName()); diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java index cf2014a1a..bd3572340 100644 --- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java +++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java @@ -16,6 +16,12 @@ package com.android.inputmethod.latin.userdictionary; +import static com.android.inputmethod.latin.userdictionary.UserDictionaryAddWordContents.EXTRA_LOCALE; +import static com.android.inputmethod.latin.userdictionary.UserDictionaryAddWordContents.EXTRA_MODE; +import static com.android.inputmethod.latin.userdictionary.UserDictionaryAddWordContents.EXTRA_WORD; +import static com.android.inputmethod.latin.userdictionary.UserDictionaryAddWordContents.MODE_EDIT; +import static com.android.inputmethod.latin.userdictionary.UserDictionaryAddWordContents.MODE_INSERT; + import com.android.inputmethod.latin.R; import android.app.ListFragment; @@ -25,7 +31,7 @@ import android.content.Intent; import android.database.Cursor; import android.os.Build; import android.os.Bundle; -import android.provider.UserDictionary; +import android.provider.UserDictionary.Words; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; @@ -48,62 +54,8 @@ import java.util.Locale; public class UserDictionarySettings extends ListFragment { - public static final boolean IS_SHORTCUT_API_SUPPORTED = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; - - private static final String[] QUERY_PROJECTION_SHORTCUT_UNSUPPORTED = - { UserDictionary.Words._ID, UserDictionary.Words.WORD}; - private static final String[] QUERY_PROJECTION_SHORTCUT_SUPPORTED = - { UserDictionary.Words._ID, UserDictionary.Words.WORD, UserDictionary.Words.SHORTCUT}; - private static final String[] QUERY_PROJECTION = - IS_SHORTCUT_API_SUPPORTED ? - QUERY_PROJECTION_SHORTCUT_SUPPORTED : QUERY_PROJECTION_SHORTCUT_UNSUPPORTED; - - // The index of the shortcut in the above array. - private static final int INDEX_SHORTCUT = 2; - - private static final String[] ADAPTER_FROM_SHORTCUT_UNSUPPORTED = { - UserDictionary.Words.WORD, - }; - - private static final String[] ADAPTER_FROM_SHORTCUT_SUPPORTED = { - UserDictionary.Words.WORD, UserDictionary.Words.SHORTCUT - }; - - private static final String[] ADAPTER_FROM = IS_SHORTCUT_API_SUPPORTED ? - ADAPTER_FROM_SHORTCUT_SUPPORTED : ADAPTER_FROM_SHORTCUT_UNSUPPORTED; - - private static final int[] ADAPTER_TO_SHORTCUT_UNSUPPORTED = { - android.R.id.text1, - }; - - private static final int[] ADAPTER_TO_SHORTCUT_SUPPORTED = { - android.R.id.text1, android.R.id.text2 - }; - - private static final int[] ADAPTER_TO = IS_SHORTCUT_API_SUPPORTED ? - ADAPTER_TO_SHORTCUT_SUPPORTED : ADAPTER_TO_SHORTCUT_UNSUPPORTED; - - // Either the locale is empty (means the word is applicable to all locales) - // or the word equals our current locale - private static final String QUERY_SELECTION = - UserDictionary.Words.LOCALE + "=?"; - private static final String QUERY_SELECTION_ALL_LOCALES = - UserDictionary.Words.LOCALE + " is null"; - - private static final String DELETE_SELECTION_WITH_SHORTCUT = UserDictionary.Words.WORD - + "=? AND " + UserDictionary.Words.SHORTCUT + "=?"; - private static final String DELETE_SELECTION_WITHOUT_SHORTCUT = UserDictionary.Words.WORD - + "=? AND " + UserDictionary.Words.SHORTCUT + " is null OR " - + UserDictionary.Words.SHORTCUT + "=''"; - private static final String DELETE_SELECTION_SHORTCUT_UNSUPPORTED = - UserDictionary.Words.WORD + "=?"; - - private static final int OPTIONS_MENU_ADD = Menu.FIRST; - private Cursor mCursor; - - protected String mLocale; + private String mLocale; @Override public void onCreate(Bundle savedInstanceState) { @@ -172,55 +124,60 @@ public class UserDictionarySettings extends ListFragment { // TODO: it should be easy to make this more readable by making the special values // human-readable, like "all_locales" and "current_locales" strings, provided they // can be guaranteed not to match locales that may exist. - if ("".equals(locale)) { + if (TextUtils.isEmpty(locale)) { // Case-insensitive sort - 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 + ")"); + return getActivity().managedQuery( + Words.CONTENT_URI, + new String[] { Words._ID, Words.WORD }, + Words.LOCALE + " is null", + null, + "UPPER(" + Words.WORD + ")"); } + return getActivity().managedQuery( + Words.CONTENT_URI, + new String[] { Words._ID, Words.WORD }, + Words.LOCALE + "=?", + new String[] { locale }, + "UPPER(" + Words.WORD + ")"); } private ListAdapter createAdapter() { - return new MyAdapter(getActivity(), R.layout.user_dictionary_item, mCursor, - ADAPTER_FROM, ADAPTER_TO, this); + return new MyAdapter( + getActivity(), + R.layout.user_dictionary_item, + mCursor, + new String[] { Words.WORD }, + new int[] { android.R.id.text1 }); } @Override public void onListItemClick(ListView l, View v, int position, long id) { final String word = getWord(position); - final String shortcut = getShortcut(position); if (word != null) { - showAddOrEditDialog(word, shortcut); + showAddOrEditDialog(word); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { final Locale systemLocale = getResources().getConfiguration().locale; if (!TextUtils.isEmpty(mLocale) && !mLocale.equals(systemLocale.toString())) { // Hide the add button for ICS because it doesn't support specifying a locale - // for an entry. This new "locale"-aware API has been added in conjunction - // with the shortcut API. + // for an entry. return; } } - MenuItem actionItem = - menu.add(0, OPTIONS_MENU_ADD, 0, R.string.user_dict_settings_add_menu_title) - .setIcon(R.drawable.ic_menu_add); - actionItem.setShowAsAction( - MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + menu.add(0, Menu.FIRST, 0, R.string.user_dict_settings_add_menu_title) + .setIcon(R.drawable.ic_menu_add) + .setShowAsAction( + MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); } @Override public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == OPTIONS_MENU_ADD) { - showAddOrEditDialog(null, null); + if (item.getItemId() == Menu.FIRST) { + showAddOrEditDialog(null); return true; } return false; @@ -229,20 +186,13 @@ public class UserDictionarySettings extends ListFragment { /** * Add or edit a word. If editingWord is null, it's an add; otherwise, it's an edit. * @param editingWord the word to edit, or null if it's an add. - * @param editingShortcut the shortcut for this entry, or null if none. */ - private void showAddOrEditDialog(final String editingWord, final String editingShortcut) { + private void showAddOrEditDialog(final String editingWord) { final Bundle args = new Bundle(); - args.putInt(UserDictionaryAddWordContents.EXTRA_MODE, null == editingWord - ? UserDictionaryAddWordContents.MODE_INSERT - : UserDictionaryAddWordContents.MODE_EDIT); - args.putString(UserDictionaryAddWordContents.EXTRA_WORD, editingWord); - args.putString(UserDictionaryAddWordContents.EXTRA_SHORTCUT, editingShortcut); - args.putString(UserDictionaryAddWordContents.EXTRA_LOCALE, mLocale); - android.preference.PreferenceActivity pa = - (android.preference.PreferenceActivity)getActivity(); - pa.startPreferencePanel(UserDictionaryAddWordFragment.class.getName(), - args, R.string.user_dict_settings_add_dialog_title, null, null, 0); + args.putInt(EXTRA_MODE, editingWord == null ? MODE_INSERT : MODE_EDIT); + args.putString(EXTRA_WORD, editingWord); + args.putString(EXTRA_LOCALE, mLocale); + getActivity(); } private String getWord(final int position) { @@ -251,85 +201,44 @@ public class UserDictionarySettings extends ListFragment { // Handle a possible race-condition if (mCursor.isAfterLast()) return null; - return mCursor.getString( - mCursor.getColumnIndexOrThrow(UserDictionary.Words.WORD)); + return mCursor.getString(mCursor.getColumnIndexOrThrow(Words.WORD)); } - private String getShortcut(final int position) { - if (!IS_SHORTCUT_API_SUPPORTED) return null; - if (null == mCursor) return null; - mCursor.moveToPosition(position); - // Handle a possible race-condition - if (mCursor.isAfterLast()) return null; - - return mCursor.getString( - mCursor.getColumnIndexOrThrow(UserDictionary.Words.SHORTCUT)); - } - - public static void deleteWord(final String word, final String shortcut, - final ContentResolver resolver) { - if (!IS_SHORTCUT_API_SUPPORTED) { - resolver.delete(UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_SHORTCUT_UNSUPPORTED, - new String[] { word }); - } else if (TextUtils.isEmpty(shortcut)) { - resolver.delete( - UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_WITHOUT_SHORTCUT, - new String[] { word }); - } else { - resolver.delete( - UserDictionary.Words.CONTENT_URI, DELETE_SELECTION_WITH_SHORTCUT, - new String[] { word, shortcut }); - } + public static void deleteWord(final String word, final ContentResolver resolver) { + resolver.delete(Words.CONTENT_URI, Words.WORD + "=?", new String[] { word }); } 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) { - if (!IS_SHORTCUT_API_SUPPORTED) { - // just let SimpleCursorAdapter set the view values - return false; - } - if (columnIndex == INDEX_SHORTCUT) { - final String shortcut = c.getString(INDEX_SHORTCUT); - if (TextUtils.isEmpty(shortcut)) { - v.setVisibility(View.GONE); - } else { - ((TextView)v).setText(shortcut); - v.setVisibility(View.VISIBLE); - } - v.invalidate(); - return true; - } - + public boolean setViewValue(final View v, final Cursor c, final int columnIndex) { + // just let SimpleCursorAdapter set the view values return false; } }; - @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); - final int wordColIndex = c.getColumnIndexOrThrow(UserDictionary.Words.WORD); + final int wordColIndex = c.getColumnIndexOrThrow(Words.WORD); mIndexer = new AlphabetIndexer(c, wordColIndex, alphabet); } setViewBinder(mViewBinder); } @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/userdictionary/UserDictionarySettingsUtils.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java index e58727ec4..c0a946e42 100644 --- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java +++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java @@ -17,7 +17,7 @@ package com.android.inputmethod.latin.userdictionary; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.utils.LocaleUtils; +import com.android.inputmethod.latin.common.LocaleUtils; import android.content.Context; import android.text.TextUtils; 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..c9ecade91 100644 --- a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java @@ -24,20 +24,22 @@ 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. } - public static boolean suggestionExceedsAutoCorrectionThreshold( - final SuggestedWordInfo suggestion, final String consideredWord, - final float autoCorrectionThreshold) { + public static boolean suggestionExceedsThreshold(final SuggestedWordInfo suggestion, + final String consideredWord, final float threshold) { if (null != suggestion) { // Shortlist a whitelisted word if (suggestion.isKindOf(SuggestedWordInfo.KIND_WHITELIST)) { return true; } + // TODO: return suggestion.isAprapreateForAutoCorrection(); + if (!suggestion.isAprapreateForAutoCorrection()) { + return false; + } final int autoCorrectionSuggestionScore = suggestion.mScore; // TODO: when the normalized score of the first suggestion is nearly equals to // the normalized score of the second suggestion, behave less aggressive. @@ -46,47 +48,15 @@ public final class AutoCorrectionUtils { if (DBG) { Log.d(TAG, "Normalized " + consideredWord + "," + suggestion + "," + autoCorrectionSuggestionScore + ", " + normalizedScore - + "(" + autoCorrectionThreshold + ")"); + + "(" + threshold + ")"); } - if (normalizedScore >= autoCorrectionThreshold) { + if (normalizedScore >= threshold) { if (DBG) { - Log.d(TAG, "Auto corrected by S-threshold."); + Log.d(TAG, "Exceeds 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..3bf9c6200 100644 --- a/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java @@ -18,9 +18,9 @@ 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; import java.io.File; import java.io.IOException; @@ -40,10 +40,10 @@ public final class BinaryDictionaryUtils { JniUtils.loadNativeLibrary(); } + @UsedForTesting 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. @@ -131,8 +123,6 @@ public final class BinaryDictionaryUtils { */ @UsedForTesting public static int setCurrentTimeForTest(final int currentTime) { - final int currentNativeTimestamp = setCurrentTimeForTestNative(currentTime); - PersonalizationHelper.currentTimeChangedForTesting(currentNativeTimestamp); - return currentNativeTimestamp; + return setCurrentTimeForTestNative(currentTime); } } 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 deleted file mode 100644 index 61292fc36..000000000 --- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java +++ /dev/null @@ -1,43 +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.ArrayList; -import java.util.Collections; -import java.util.Map; -import java.util.TreeMap; - -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(); - } - if (start < 0 || start > end || end > array.length) { - throw new IllegalArgumentException(); - } - - final ArrayList<E> list = new ArrayList<>(end - start); - for (int i = start; i < end; i++) { - list.add(array[i]); - } - return list; - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java index 34f59e8bc..5c0c4328f 100644 --- a/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java @@ -17,8 +17,8 @@ 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; import java.util.HashMap; @@ -26,14 +26,16 @@ 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 SHORTCUT_TAG = "shortcut"; + public static final String NGRAM_TAG = "ngram"; + public static final String NGRAM_PREV_WORD_TAG = "prev_word"; public static final String PROBABILITY_TAG = "f"; public static final String HISTORICAL_INFO_TAG = "historicalInfo"; public static final String HISTORICAL_INFO_SEPARATOR = ":"; 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,29 +60,29 @@ 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) { - for (final WeightedString shortcutTarget : wordProperty.mShortcutTargets) { - builder.append(" " + SHORTCUT_TAG + "=" + shortcutTarget.mWord); + if (wordProperty.mHasNgrams) { + for (final NgramProperty ngramProperty : wordProperty.mNgrams) { + builder.append(" " + NGRAM_TAG + "=" + ngramProperty.mTargetWord.mWord); builder.append(","); - builder.append(formatProbabilityInfo(shortcutTarget.mProbabilityInfo)); - builder.append("\n"); - } - } - if (wordProperty.mBigrams != null) { - for (final WeightedString bigram : wordProperty.mBigrams) { - builder.append(" " + BIGRAM_TAG + "=" + bigram.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.isNthPrevWordBeginningOfSentence(i + 1)) { + builder.append("," + BEGINNING_OF_SENTENCE_TAG + "=true"); + } + builder.append("\n"); + } } } return builder.toString(); @@ -100,4 +102,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 deleted file mode 100644 index 87df013a6..000000000 --- a/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java +++ /dev/null @@ -1,91 +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; - -public final class CoordinateUtils { - private static final int INDEX_X = 0; - private static final int INDEX_Y = 1; - private static final int ELEMENT_SIZE = INDEX_Y + 1; - - private CoordinateUtils() { - // This utility class is not publicly instantiable. - } - - public static int[] newInstance() { - return new int[ELEMENT_SIZE]; - } - - public static int x(final int[] coords) { - return coords[INDEX_X]; - } - - public static int y(final int[] coords) { - return coords[INDEX_Y]; - } - - public static void set(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) { - destination[INDEX_X] = source[INDEX_X]; - destination[INDEX_Y] = source[INDEX_Y]; - } - - public static int[] newCoordinateArray(final int arraySize) { - return new int[ELEMENT_SIZE * arraySize]; - } - - public static int[] newCoordinateArray(final int arraySize, - final int defaultX, final int defaultY) { - final int[] result = new int[ELEMENT_SIZE * arraySize]; - for (int i = 0; i < arraySize; ++i) { - setXYInArray(result, i, defaultX, defaultY); - } - return result; - } - - public static int xFromArray(final int[] coordsArray, final int index) { - return coordsArray[ELEMENT_SIZE * index + INDEX_X]; - } - - public static int yFromArray(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); - } - - public static void setXYInArray(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]; - } -} 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..25fa723cc 100644 --- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -22,11 +22,15 @@ import android.content.res.AssetManager; import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; +import android.view.inputmethod.InputMethodSubtype; +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.RichInputMethodManager; +import com.android.inputmethod.latin.common.LocaleUtils; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.makedict.UnsupportedFormatException; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; @@ -35,9 +39,13 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * This class encapsulates the logic for the Latin-IME side of dictionary information management. */ @@ -46,6 +54,7 @@ public class DictionaryInfoUtils { private static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName(); private static final String DEFAULT_MAIN_DICT = "main"; private static final String MAIN_DICT_PREFIX = "main_"; + private static final String DECODER_DICT_SUFFIX = DecoderSpecificConstants.DECODER_DICT_SUFFIX; // 6 digits - unicode is limited to 21 bits private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6; @@ -57,28 +66,36 @@ public class DictionaryInfoUtils { private static final String DATE_COLUMN = "date"; private static final String FILESIZE_COLUMN = "filesize"; private static final String VERSION_COLUMN = "version"; + @Nonnull public final String mId; + @Nonnull public final Locale mLocale; + @Nullable public final String mDescription; public final AssetFileAddress mFileAddress; public final int mVersion; - public DictionaryInfo(final String id, final Locale locale, final String description, - final AssetFileAddress fileAddress, final int version) { + + public DictionaryInfo(@Nonnull final String id, @Nonnull final Locale locale, + @Nullable final String description, @Nullable final AssetFileAddress fileAddress, + final int version) { mId = id; mLocale = locale; mDescription = description; mFileAddress = fileAddress; mVersion = version; } + public ContentValues toContentValues() { final ContentValues values = new ContentValues(); values.put(WORDLISTID_COLUMN, mId); values.put(LOCALE_COLUMN, mLocale.toString()); values.put(DESCRIPTION_COLUMN, mDescription); - values.put(LOCAL_FILENAME_COLUMN, mFileAddress.mFilename); + values.put(LOCAL_FILENAME_COLUMN, + mFileAddress != null ? mFileAddress.mFilename : ""); values.put(DATE_COLUMN, TimeUnit.MILLISECONDS.toSeconds( - new File(mFileAddress.mFilename).lastModified())); - values.put(FILESIZE_COLUMN, mFileAddress.mLength); + mFileAddress != null ? new File(mFileAddress.mFilename).lastModified() : 0)); + values.put(FILESIZE_COLUMN, + mFileAddress != null ? mFileAddress.mLength : 0); values.put(VERSION_COLUMN, mVersion); return values; } @@ -140,9 +157,10 @@ public class DictionaryInfoUtils { } /** - * Reverse escaping done by replaceFileNameDangerousCharacters. + * Reverse escaping done by {@link #replaceFileNameDangerousCharacters(String)}. */ - public static String getWordListIdFromFileName(final String fname) { + @Nonnull + public static String getWordListIdFromFileName(@Nonnull final String fname) { final StringBuilder sb = new StringBuilder(); final int fnameLength = fname.length(); for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) { @@ -174,12 +192,15 @@ public class DictionaryInfoUtils { * {@link #getMainDictId(Locale)} and {@link #isMainWordListId(String)}. * @return The category as a string or null if it can't be found in the file name. */ - public static String getCategoryFromFileName(final String fileName) { + @Nullable + public static String getCategoryFromFileName(@Nonnull final String fileName) { final String id = getWordListIdFromFileName(fileName); final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR); // An id is supposed to be in format category:locale, so splitting on the separator // should yield a 2-elements array - if (2 != idArray.length) return null; + if (2 != idArray.length) { + return null; + } return idArray[0]; } @@ -223,12 +244,26 @@ public class DictionaryInfoUtils { final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR); // An id is supposed to be in format category:locale, so splitting on the separator // should yield a 2-elements array - if (2 != idArray.length) return false; + if (2 != idArray.length) { + return false; + } return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY.equals(idArray[0]); } /** + * Find out whether a dictionary is available for this locale. + * @param context the context on which to check resources. + * @param locale the locale to check for. + * @return whether a (non-placeholder) dictionary is available or not. + */ + public static boolean isDictionaryAvailable(final Context context, final Locale locale) { + final Resources res = context.getResources(); + return 0 != getMainDictionaryResourceIdIfAvailableForLocale(res, locale); + } + + /** * 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 */ @@ -237,8 +272,8 @@ public class DictionaryInfoUtils { int resId; // Try to find main_language_country dictionary. if (!locale.getCountry().isEmpty()) { - final String dictLanguageCountry = - MAIN_DICT_PREFIX + locale.toString().toLowerCase(Locale.ROOT); + final String dictLanguageCountry = MAIN_DICT_PREFIX + + locale.toString().toLowerCase(Locale.ROOT) + DECODER_DICT_SUFFIX; if ((resId = res.getIdentifier( dictLanguageCountry, "raw", RESOURCE_PACKAGE_NAME)) != 0) { return resId; @@ -246,7 +281,7 @@ public class DictionaryInfoUtils { } // Try to find main_language dictionary. - final String dictLanguage = MAIN_DICT_PREFIX + locale.getLanguage(); + final String dictLanguage = MAIN_DICT_PREFIX + locale.getLanguage() + DECODER_DICT_SUFFIX; if ((resId = res.getIdentifier(dictLanguage, "raw", RESOURCE_PACKAGE_NAME)) != 0) { return resId; } @@ -257,13 +292,17 @@ public class DictionaryInfoUtils { /** * Returns a main dictionary resource id + * @param res resources for the app * @param locale dictionary locale * @return main dictionary resource id */ public static int getMainDictionaryResourceId(final Resources res, final Locale locale) { int resourceId = getMainDictionaryResourceIdIfAvailableForLocale(res, locale); - if (0 != resourceId) return resourceId; - return res.getIdentifier(DEFAULT_MAIN_DICT, "raw", RESOURCE_PACKAGE_NAME); + if (0 != resourceId) { + return resourceId; + } + return res.getIdentifier(DEFAULT_MAIN_DICT + DecoderSpecificConstants.DECODER_DICT_SUFFIX, + "raw", RESOURCE_PACKAGE_NAME); } /** @@ -274,19 +313,15 @@ public class DictionaryInfoUtils { * unique ID to them. This ID is just the name of the language (locale-wise) they * are for, and this method returns this ID. */ - public static String getMainDictId(final Locale locale) { + public static String getMainDictId(@Nonnull final Locale locale) { // This works because we don't include by default different dictionaries for // different countries. This actually needs to return the id that we would // like to use for word lists included in resources, and the following is okay. return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY + - BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.getLanguage().toString(); + BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.toString().toLowerCase(); } - public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file) { - return getDictionaryFileHeaderOrNull(file, 0, file.length()); - } - - private static DictionaryHeader getDictionaryFileHeaderOrNull(final File file, + public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file, final long offset, final long length) { try { final DictionaryHeader header = @@ -303,20 +338,27 @@ public class DictionaryInfoUtils { * Returns information of the dictionary. * * @param fileAddress the asset dictionary file address. + * @param locale Locale for this file. * @return information of the specified dictionary. */ private static DictionaryInfo createDictionaryInfoFromFileAddress( - final AssetFileAddress fileAddress) { - final DictionaryHeader header = getDictionaryFileHeaderOrNull( - new File(fileAddress.mFilename), fileAddress.mOffset, fileAddress.mLength); - if (header == null) { - return null; - } - final String id = header.getId(); - final Locale locale = LocaleUtils.constructLocaleFromString(header.getLocaleString()); - final String description = header.getDescription(); - final String version = header.getVersion(); - return new DictionaryInfo(id, locale, description, fileAddress, Integer.parseInt(version)); + final AssetFileAddress fileAddress, Locale locale) { + final String id = getMainDictId(locale); + final int version = DictionaryHeaderUtils.getContentVersion(fileAddress); + final String description = SubtypeLocaleUtils + .getSubtypeLocaleDisplayName(locale.toString()); + return new DictionaryInfo(id, locale, description, fileAddress, version); + } + + /** + * Returns dictionary information for the given locale. + */ + private static DictionaryInfo createDictionaryInfoFromLocale(Locale locale) { + final String id = getMainDictId(locale); + final int version = -1; + final String description = SubtypeLocaleUtils + .getSubtypeLocaleDisplayName(locale.toString()); + return new DictionaryInfo(id, locale, description, null, version); } private static void addOrUpdateDictInfo(final ArrayList<DictionaryInfo> dictList, @@ -343,18 +385,23 @@ public class DictionaryInfoUtils { if (null != directoryList) { for (final File directory : directoryList) { final String localeString = getWordListIdFromFileName(directory.getName()); - File[] dicts = BinaryDictionaryGetter.getCachedWordLists(localeString, context); + final File[] dicts = BinaryDictionaryGetter.getCachedWordLists( + localeString, context); for (final File dict : dicts) { final String wordListId = getWordListIdFromFileName(dict.getName()); - if (!DictionaryInfoUtils.isMainWordListId(wordListId)) continue; + if (!DictionaryInfoUtils.isMainWordListId(wordListId)) { + continue; + } final Locale locale = LocaleUtils.constructLocaleFromString(localeString); final AssetFileAddress fileAddress = AssetFileAddress.makeFromFile(dict); final DictionaryInfo dictionaryInfo = - createDictionaryInfoFromFileAddress(fileAddress); + createDictionaryInfoFromFileAddress(fileAddress, locale); // Protect against cases of a less-specific dictionary being found, like an // en dictionary being used for an en_US locale. In this case, the en dictionary // should be used for en_US but discounted for listing purposes. - if (dictionaryInfo == null || !dictionaryInfo.mLocale.equals(locale)) continue; + if (dictionaryInfo == null || !dictionaryInfo.mLocale.equals(locale)) { + continue; + } addOrUpdateDictInfo(dictList, dictionaryInfo); } } @@ -368,25 +415,45 @@ public class DictionaryInfoUtils { final int resourceId = DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale( context.getResources(), locale); - if (0 == resourceId) continue; + if (0 == resourceId) { + continue; + } final AssetFileAddress fileAddress = BinaryDictionaryGetter.loadFallbackResource(context, resourceId); - final DictionaryInfo dictionaryInfo = createDictionaryInfoFromFileAddress(fileAddress); + final DictionaryInfo dictionaryInfo = createDictionaryInfoFromFileAddress(fileAddress, + locale); // Protect against cases of a less-specific dictionary being found, like an // en dictionary being used for an en_US locale. In this case, the en dictionary // should be used for en_US but discounted for listing purposes. - if (!dictionaryInfo.mLocale.equals(locale)) continue; + // TODO: Remove dictionaryInfo == null when the static LMs have the headers. + if (dictionaryInfo == null || !dictionaryInfo.mLocale.equals(locale)) { + continue; + } + addOrUpdateDictInfo(dictList, dictionaryInfo); + } + + // Generate the dictionary information from the enabled subtypes. This will not + // overwrite the real records. + RichInputMethodManager.init(context); + List<InputMethodSubtype> enabledSubtypes = RichInputMethodManager + .getInstance().getMyEnabledInputMethodSubtypeList(true); + for (InputMethodSubtype subtype : enabledSubtypes) { + Locale locale = LocaleUtils.constructLocaleFromString(subtype.getLocale()); + DictionaryInfo dictionaryInfo = createDictionaryInfoFromLocale(locale); addOrUpdateDictInfo(dictList, dictionaryInfo); } return dictList; } + @UsedForTesting public static boolean looksValidForDictionaryInsertion(final CharSequence text, final SpacingAndPunctuations spacingAndPunctuations) { - if (TextUtils.isEmpty(text)) return false; + if (TextUtils.isEmpty(text)) { + return false; + } final int length = text.length(); - if (length > Constants.DICTIONARY_MAX_WORD_LENGTH) { + if (length > DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH) { return false; } int i = 0; @@ -400,7 +467,9 @@ public class DictionaryInfoUtils { digitCount += charCount; continue; } - if (!spacingAndPunctuations.isWordCodePoint(codePoint)) return false; + if (!spacingAndPunctuations.isWordCodePoint(codePoint)) { + return false; + } } // We reject strings entirely comprised of digits to avoid using PIN codes or credit // card numbers. It would come in handy for word prediction though; a good example is diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java deleted file mode 100644 index 787e4a59d..000000000 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.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 java.util.List; -import java.util.Locale; - -import android.view.inputmethod.InputMethodSubtype; - -import com.android.inputmethod.latin.PrevWordsInfo; - -public interface DistracterFilter { - /** - * Determine whether a word is a distracter to words in dictionaries. - * - * @param prevWordsInfo the information of previous words. - * @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, - final String testedWord, final Locale locale); - - public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes); - - public void close(); - - public static final DistracterFilter EMPTY_DISTRACTER_FILTER = new DistracterFilter() { - @Override - public boolean isDistracterToWordsInDictionaries(PrevWordsInfo prevWordsInfo, - String testedWord, Locale locale) { - return false; - } - - @Override - public void close() { - } - - @Override - public void updateEnabledSubtypes(List<InputMethodSubtype> enabledSubtypes) { - } - }; -} diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java deleted file mode 100644 index 27973287d..000000000 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java +++ /dev/null @@ -1,286 +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 java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import android.content.Context; -import android.content.res.Resources; -import android.text.InputType; -import android.util.Log; -import android.util.LruCache; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodSubtype; - -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.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.WordComposer; -import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; - -/** - * This class is used to prevent distracters being added to personalization - * or user history dictionaries - */ -public class DistracterFilterCheckingExactMatchesAndSuggestions implements DistracterFilter { - private static final String TAG = - 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 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 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 - // 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 - // the dictionary. - private static final float DISTRACTER_WORD_SCORE_THRESHOLD = 0.4f; - - /** - * Create a DistracterFilter instance. - * - * @param context the context. - */ - public DistracterFilterCheckingExactMatchesAndSuggestions(final Context context) { - mContext = context; - mLocaleToSubtypeMap = new HashMap<>(); - mLocaleToKeyboardMap = new HashMap<>(); - mDictionaryFacilitator = new DictionaryFacilitator(); - mDistractersCache = new LruCache<>(MAX_DISTRACTERS_CACHE_SIZE); - mKeyboard = null; - } - - @Override - public void close() { - mDictionaryFacilitator.closeDictionaries(); - } - - @Override - public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { - final Map<Locale, InputMethodSubtype> newLocaleToSubtypeMap = new HashMap<>(); - if (enabledSubtypes != null) { - for (final InputMethodSubtype subtype : enabledSubtypes) { - final Locale locale = SubtypeLocaleUtils.getSubtypeLocale(subtype); - if (newLocaleToSubtypeMap.containsKey(locale)) { - // Multiple subtypes are enabled for one locale. - // TODO: Investigate what we should do for this case. - continue; - } - newLocaleToSubtypeMap.put(locale, subtype); - } - } - if (mLocaleToSubtypeMap.equals(newLocaleToSubtypeMap)) { - // Enabled subtypes have not been changed. - return; - } - synchronized (mLock) { - mLocaleToSubtypeMap.clear(); - mLocaleToSubtypeMap.putAll(newLocaleToSubtypeMap); - mLocaleToKeyboardMap.clear(); - } - } - - private void loadKeyboardForLocale(final Locale newLocale) { - final Keyboard cachedKeyboard = mLocaleToKeyboardMap.get(newLocale); - if (cachedKeyboard != null) { - mKeyboard = cachedKeyboard; - return; - } - final InputMethodSubtype subtype; - synchronized (mLock) { - subtype = mLocaleToSubtypeMap.get(newLocale); - } - if (subtype == null) { - return; - } - final EditorInfo editorInfo = new EditorInfo(); - editorInfo.inputType = InputType.TYPE_CLASS_TEXT; - final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( - mContext, editorInfo); - final Resources res = mContext.getResources(); - final int keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res); - final int keyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res); - builder.setKeyboardGeometry(keyboardWidth, keyboardHeight); - builder.setSubtype(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); - } - - /** - * Determine whether a word is a distracter to words in dictionaries. - * - * @param prevWordsInfo the information of previous words. 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, - 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 (DEBUG) { - Log.d(TAG, "testedWord: " + testedWord); - } - final Boolean isCachedDistracter = mDistractersCache.get(testedWord); - if (isCachedDistracter != null && isCachedDistracter) { - if (DEBUG) { - Log.d(TAG, "isDistracter: true (cache hit)"); - } - return true; - } - - final boolean isDistracterCheckedByGetMaxFreqencyOfExactMatches = - checkDistracterUsingMaxFreqencyOfExactMatches(testedWord); - if (isDistracterCheckedByGetMaxFreqencyOfExactMatches) { - // Add the word to the cache. - mDistractersCache.put(testedWord, Boolean.TRUE); - return true; - } - final boolean isValidWord = mDictionaryFacilitator.isValidWord(testedWord, - false /* ignoreCase */); - if (isValidWord) { - // Valid word is not a distractor. - if (DEBUG) { - Log.d(TAG, "isDistracter: false (valid word)"); - } - return false; - } - - final boolean isDistracterCheckedByGetSuggestion = - checkDistracterUsingGetSuggestions(testedWord); - if (isDistracterCheckedByGetSuggestion) { - // Add the word to the cache. - mDistractersCache.put(testedWord, Boolean.TRUE); - return true; - } - return false; - } - - private boolean checkDistracterUsingMaxFreqencyOfExactMatches(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 boolean isDistracter = perfectMatchFreq < exactMatchFreq; - if (DEBUG) { - Log.d(TAG, "perfectMatchFreq: " + perfectMatchFreq); - Log.d(TAG, "exactMatchFreq: " + exactMatchFreq); - Log.d(TAG, "isDistracter: " + isDistracter); - } - return isDistracter; - } - - private boolean checkDistracterUsingGetSuggestions(final String testedWord) { - if (mKeyboard == null) { - return false; - } - final SettingsValuesForSuggestion settingsValuesForSuggestion = - new SettingsValuesForSuggestion(false /* blockPotentiallyOffensive */, - false /* spaceAwareGestureEnabled */, - null /* additionalFeaturesSettingValues */); - final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(testedWord); - final String consideredWord = trailingSingleQuotesCount > 0 ? - testedWord.substring(0, testedWord.length() - trailingSingleQuotesCount) : - testedWord; - final WordComposer composer = new WordComposer(); - final int[] codePoints = StringUtils.toCodePointArray(testedWord); - - 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(), - 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; - } - } - - private static boolean suggestionExceedsDistracterThreshold(final SuggestedWordInfo suggestion, - final String consideredWord, final float distracterThreshold) { - if (suggestion == null) { - return false; - } - final int suggestionScore = suggestion.mScore; - final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( - consideredWord, suggestion.mWord, suggestionScore); - if (DEBUG) { - Log.d(TAG, "normalizedScore: " + normalizedScore); - Log.d(TAG, "distracterThreshold: " + distracterThreshold); - } - if (normalizedScore > distracterThreshold) { - return true; - } - return false; - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java deleted file mode 100644 index 4ad4ba784..000000000 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java +++ /dev/null @@ -1,59 +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 java.util.List; -import java.util.Locale; - -import android.view.inputmethod.InputMethodSubtype; - -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.PrevWordsInfo; - -public class DistracterFilterCheckingIsInDictionary implements DistracterFilter { - private final DistracterFilter mDistracterFilter; - private final Dictionary mDictionary; - - public DistracterFilterCheckingIsInDictionary(final DistracterFilter distracterFilter, - final Dictionary dictionary) { - mDistracterFilter = distracterFilter; - mDictionary = dictionary; - } - - @Override - public boolean isDistracterToWordsInDictionaries(PrevWordsInfo prevWordsInfo, - 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); - } - } - - @Override - public void updateEnabledSubtypes(List<InputMethodSubtype> enabledSubtypes) { - // Do nothing. - } - - @Override - public void close() { - // Do nothing. - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java index 61da1b789..8ce6eff92 100644 --- a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java @@ -16,64 +16,136 @@ package com.android.inputmethod.latin.utils; +import android.util.Log; + import com.android.inputmethod.annotations.UsedForTesting; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; +import java.lang.Thread.UncaughtExceptionHandler; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; /** * Utilities to manage executors. */ public class ExecutorUtils { - private static final ConcurrentHashMap<String, ExecutorService> sExecutorMap = - new ConcurrentHashMap<>(); - private static class ThreadFactoryWithId implements ThreadFactory { - private final String mId; + private static final String TAG = "ExecutorUtils"; + + public static final String KEYBOARD = "Keyboard"; + public static final String SPELLING = "Spelling"; + + private static ScheduledExecutorService sKeyboardExecutorService = newExecutorService(KEYBOARD); + private static ScheduledExecutorService sSpellingExecutorService = newExecutorService(SPELLING); + + private static ScheduledExecutorService newExecutorService(final String name) { + return Executors.newSingleThreadScheduledExecutor(new ExecutorFactory(name)); + } - public ThreadFactoryWithId(final String id) { - mId = id; + private static class ExecutorFactory implements ThreadFactory { + private final String mName; + + private ExecutorFactory(final String name) { + mName = name; } @Override - public Thread newThread(final Runnable r) { - return new Thread(r, "Executor - " + mId); + public Thread newThread(final Runnable runnable) { + Thread thread = new Thread(runnable, TAG); + thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread thread, Throwable ex) { + Log.w(mName + "-" + runnable.getClass().getSimpleName(), ex); + } + }); + return thread; } } + @UsedForTesting + private static ScheduledExecutorService sExecutorServiceForTests; + + @UsedForTesting + public static void setExecutorServiceForTests( + final ScheduledExecutorService executorServiceForTests) { + sExecutorServiceForTests = executorServiceForTests; + } + + // + // Public methods used to schedule a runnable for execution. + // + /** - * Gets the executor for the given id. + * @param name Executor's name. + * @return scheduled executor service used to run background tasks */ - public static ExecutorService getExecutor(final String id) { - ExecutorService executor = sExecutorMap.get(id); - if (executor == null) { - synchronized(sExecutorMap) { - executor = sExecutorMap.get(id); - if (executor == null) { - executor = Executors.newSingleThreadExecutor(new ThreadFactoryWithId(id)); - sExecutorMap.put(id, executor); - } - } + public static ScheduledExecutorService getBackgroundExecutor(final String name) { + if (sExecutorServiceForTests != null) { + return sExecutorServiceForTests; + } + switch (name) { + case KEYBOARD: + return sKeyboardExecutorService; + case SPELLING: + return sSpellingExecutorService; + default: + throw new IllegalArgumentException("Invalid executor: " + name); } - return executor; } - /** - * Shutdowns all executors and removes all executors from the executor map for testing. - */ + public static void killTasks(final String name) { + final ScheduledExecutorService executorService = getBackgroundExecutor(name); + executorService.shutdownNow(); + try { + executorService.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Log.wtf(TAG, "Failed to shut down: " + name); + } + if (executorService == sExecutorServiceForTests) { + // Don't do anything to the test service. + return; + } + switch (name) { + case KEYBOARD: + sKeyboardExecutorService = newExecutorService(KEYBOARD); + break; + case SPELLING: + sSpellingExecutorService = newExecutorService(SPELLING); + break; + default: + throw new IllegalArgumentException("Invalid executor: " + name); + } + } + + @UsedForTesting + public static Runnable chain(final Runnable... runnables) { + return new RunnableChain(runnables); + } + @UsedForTesting - public static void shutdownAllExecutors() { - synchronized(sExecutorMap) { - for (final ExecutorService executor : sExecutorMap.values()) { - executor.execute(new Runnable() { - @Override - public void run() { - executor.shutdown(); - sExecutorMap.remove(executor); - } - }); + public static class RunnableChain implements Runnable { + private final Runnable[] mRunnables; + + private RunnableChain(final Runnable... runnables) { + if (runnables == null || runnables.length == 0) { + throw new IllegalArgumentException("Attempting to construct an empty chain"); + } + mRunnables = runnables; + } + + @UsedForTesting + public Runnable[] getRunnables() { + return mRunnables; + } + + @Override + public void run() { + for (Runnable runnable : mRunnables) { + if (Thread.interrupted()) { + return; + } + runnable.run(); } } } diff --git a/java/src/com/android/inputmethod/latin/utils/FileUtils.java b/java/src/com/android/inputmethod/latin/utils/FileUtils.java deleted file mode 100644 index f1106a6c6..000000000 --- a/java/src/com/android/inputmethod/latin/utils/FileUtils.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2013 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.io.File; -import java.io.FilenameFilter; - -/** - * A simple class to help with removing directories recursively. - */ -public class FileUtils { - public static boolean deleteRecursively(final File path) { - if (path.isDirectory()) { - final File[] files = path.listFiles(); - if (files != null) { - for (final File child : files) { - deleteRecursively(child); - } - } - } - return path.delete(); - } - - public static boolean deleteFilteredFiles(final File dir, final FilenameFilter fileNameFilter) { - if (!dir.isDirectory()) { - return false; - } - final File[] files = dir.listFiles(fileNameFilter); - if (files == null) { - return false; - } - boolean hasDeletedAllFiles = true; - for (final File file : files) { - if (!deleteRecursively(file)) { - hasDeletedAllFiles = false; - } - } - return hasDeletedAllFiles; - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java index c2167a76b..c87a7c05c 100644 --- a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java @@ -18,18 +18,17 @@ 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; import com.android.inputmethod.latin.settings.CustomInputStyleSettingsFragment; import com.android.inputmethod.latin.settings.DebugSettingsFragment; import com.android.inputmethod.latin.settings.GestureSettingsFragment; -import com.android.inputmethod.latin.settings.MultiLingualSettingsFragment; import com.android.inputmethod.latin.settings.PreferencesSettingsFragment; import com.android.inputmethod.latin.settings.SettingsFragment; import com.android.inputmethod.latin.settings.ThemeSettingsFragment; import com.android.inputmethod.latin.spellcheck.SpellCheckerSettingsFragment; -import com.android.inputmethod.latin.userdictionary.UserDictionaryAddWordFragment; import com.android.inputmethod.latin.userdictionary.UserDictionaryList; import com.android.inputmethod.latin.userdictionary.UserDictionaryLocalePicker; import com.android.inputmethod.latin.userdictionary.UserDictionarySettings; @@ -42,9 +41,9 @@ 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()); sLatinImeFragments.add(CustomInputStyleSettingsFragment.class.getName()); sLatinImeFragments.add(GestureSettingsFragment.class.getName()); sLatinImeFragments.add(CorrectionSettingsFragment.class.getName()); @@ -52,7 +51,6 @@ public class FragmentUtils { sLatinImeFragments.add(DebugSettingsFragment.class.getName()); sLatinImeFragments.add(SettingsFragment.class.getName()); sLatinImeFragments.add(SpellCheckerSettingsFragment.class.getName()); - sLatinImeFragments.add(UserDictionaryAddWordFragment.class.getName()); sLatinImeFragments.add(UserDictionaryList.class.getName()); sLatinImeFragments.add(UserDictionaryLocalePicker.class.getName()); sLatinImeFragments.add(UserDictionarySettings.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..df0cd8437 100644 --- a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java @@ -25,6 +25,7 @@ import android.util.Log; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.settings.SettingsValues; import java.util.concurrent.TimeUnit; @@ -53,7 +54,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 +86,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; } @@ -103,7 +106,12 @@ public final class ImportantNoticeUtils { return elapsedTime >= TIMEOUT_OF_IMPORTANT_NOTICE; } - public static boolean shouldShowImportantNotice(final Context context) { + public static boolean shouldShowImportantNotice(final Context context, + final SettingsValues settingsValues) { + // Check to see whether personalization is enabled by the user. + if (!settingsValues.isPersonalizationEnabled()) { + return false; + } if (!hasNewImportantNotice(context)) { return false; } 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/LanguageOnSpacebarUtils.java b/java/src/com/android/inputmethod/latin/utils/LanguageOnSpacebarUtils.java new file mode 100644 index 000000000..a5a1ea921 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/LanguageOnSpacebarUtils.java @@ -0,0 +1,92 @@ +/* + * 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; + +import com.android.inputmethod.latin.RichInputMethodSubtype; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import javax.annotation.Nonnull; + +/** + * This class determines that the language name on the spacebar should be displayed in what format. + */ +public final class LanguageOnSpacebarUtils { + public static final int FORMAT_TYPE_NONE = 0; + public static final int FORMAT_TYPE_LANGUAGE_ONLY = 1; + public static final int FORMAT_TYPE_FULL_LOCALE = 2; + + private static List<InputMethodSubtype> sEnabledSubtypes = Collections.emptyList(); + private static boolean sIsSystemLanguageSameAsInputLanguage; + + private LanguageOnSpacebarUtils() { + // This utility class is not publicly instantiable. + } + + public static int getLanguageOnSpacebarFormatType( + @Nonnull final RichInputMethodSubtype subtype) { + if (subtype.isNoLanguage()) { + return FORMAT_TYPE_FULL_LOCALE; + } + // Only this subtype is enabled and equals to the system locale. + if (sEnabledSubtypes.size() < 2 && sIsSystemLanguageSameAsInputLanguage) { + return FORMAT_TYPE_NONE; + } + final Locale locale = subtype.getLocale(); + if (locale == null) { + return FORMAT_TYPE_NONE; + } + final String keyboardLanguage = locale.getLanguage(); + final String keyboardLayout = subtype.getKeyboardLayoutSetName(); + int sameLanguageAndLayoutCount = 0; + for (final InputMethodSubtype ims : sEnabledSubtypes) { + final String language = SubtypeLocaleUtils.getSubtypeLocale(ims).getLanguage(); + if (keyboardLanguage.equals(language) && keyboardLayout.equals( + SubtypeLocaleUtils.getKeyboardLayoutSetName(ims))) { + sameLanguageAndLayoutCount++; + } + } + // Display full locale name only when there are multiple subtypes that have the same + // locale and keyboard layout. Otherwise displaying language name is enough. + return sameLanguageAndLayoutCount > 1 ? FORMAT_TYPE_FULL_LOCALE + : FORMAT_TYPE_LANGUAGE_ONLY; + } + + public static void setEnabledSubtypes(@Nonnull final List<InputMethodSubtype> enabledSubtypes) { + sEnabledSubtypes = enabledSubtypes; + } + + public static void onSubtypeChanged(@Nonnull final RichInputMethodSubtype subtype, + final boolean implicitlyEnabledSubtype, @Nonnull final Locale systemLocale) { + final Locale newLocale = subtype.getLocale(); + if (systemLocale.equals(newLocale)) { + sIsSystemLanguageSameAsInputLanguage = true; + return; + } + if (!systemLocale.getLanguage().equals(newLocale.getLanguage())) { + sIsSystemLanguageSameAsInputLanguage = false; + return; + } + // If the subtype is enabled explicitly, the language name should be displayed even when + // the keyboard language and the system language are equal. + sIsSystemLanguageSameAsInputLanguage = implicitlyEnabledSubtype; + } +} 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/LocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java deleted file mode 100644 index c519a0de6..000000000 --- a/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import android.text.TextUtils; - -import java.util.HashMap; -import java.util.Locale; - -/** - * A class to help with handling Locales in string form. - * - * This file has the same meaning and features (and shares all of its code) with - * the one in the dictionary pack. They need to be kept synchronized; for any - * update/bugfix to this file, consider also updating/fixing the version in the - * dictionary pack. - */ -public final class LocaleUtils { - private LocaleUtils() { - // Intentional empty constructor for utility class. - } - - // Locale match level constants. - // A higher level of match is guaranteed to have a higher numerical value. - // Some room is left within constants to add match cases that may arise necessary - // in the future, for example differentiating between the case where the countries - // are both present and different, and the case where one of the locales does not - // specify the countries. This difference is not needed now. - - // Nothing matches. - public static final int LOCALE_NO_MATCH = 0; - // The languages matches, but the country are different. Or, the reference locale requires a - // country and the tested locale does not have one. - public static final int LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER = 3; - // The languages and country match, but the variants are different. Or, the reference locale - // requires a variant and the tested locale does not have one. - public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER = 6; - // The required locale is null or empty so it will accept anything, and the tested locale - // is non-null and non-empty. - public static final int LOCALE_ANY_MATCH = 10; - // The language matches, and the tested locale specifies a country but the reference locale - // does not require one. - public static final int LOCALE_LANGUAGE_MATCH = 15; - // The language and the country match, and the tested locale specifies a variant but the - // reference locale does not require one. - public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH = 20; - // The compared locales are fully identical. This is the best match level. - public static final int LOCALE_FULL_MATCH = 30; - - // The level at which a match is "normally" considered a locale match with standard algorithms. - // Don't use this directly, use #isMatch to test. - private static final int LOCALE_MATCH = LOCALE_ANY_MATCH; - - // Make this match the maximum match level. If this evolves to have more than 2 digits - // when written in base 10, also adjust the getMatchLevelSortedString method. - private static final int MATCH_LEVEL_MAX = 30; - - /** - * Return how well a tested locale matches a reference locale. - * - * This will check the tested locale against the reference locale and return a measure of how - * a well it matches the reference. The general idea is that the tested locale has to match - * every specified part of the required locale. A full match occur when they are equal, a - * partial match when the tested locale agrees with the reference locale but is more specific, - * and a difference when the tested locale does not comply with all requirements from the - * reference locale. - * In more detail, if the reference locale specifies at least a language and the testedLocale - * does not specify one, or specifies a different one, LOCALE_NO_MATCH is returned. If the - * reference locale is empty or null, it will match anything - in the form of LOCALE_FULL_MATCH - * if the tested locale is empty or null, and LOCALE_ANY_MATCH otherwise. If the reference and - * tested locale agree on the language, but not on the country, - * LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER is returned if the reference locale specifies a country, - * and LOCALE_LANGUAGE_MATCH otherwise. - * If they agree on both the language and the country, but not on the variant, - * LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER is returned if the reference locale - * specifies a variant, and LOCALE_LANGUAGE_AND_COUNTRY_MATCH otherwise. If everything matches, - * LOCALE_FULL_MATCH is returned. - * Examples: - * en <=> en_US => LOCALE_LANGUAGE_MATCH - * en_US <=> en => LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER - * en_US_POSIX <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER - * en_US <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH - * sp_US <=> en_US => LOCALE_NO_MATCH - * de <=> de => LOCALE_FULL_MATCH - * en_US <=> en_US => LOCALE_FULL_MATCH - * "" <=> en_US => LOCALE_ANY_MATCH - * - * @param referenceLocale the reference locale to test against. - * @param testedLocale the locale to test. - * @return a constant that measures how well the tested locale matches the reference locale. - */ - public static int getMatchLevel(String referenceLocale, String testedLocale) { - if (TextUtils.isEmpty(referenceLocale)) { - return TextUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH; - } - if (null == testedLocale) return LOCALE_NO_MATCH; - String[] referenceParams = referenceLocale.split("_", 3); - String[] testedParams = testedLocale.split("_", 3); - // By spec of String#split, [0] cannot be null and length cannot be 0. - if (!referenceParams[0].equals(testedParams[0])) return LOCALE_NO_MATCH; - switch (referenceParams.length) { - case 1: - return 1 == testedParams.length ? LOCALE_FULL_MATCH : LOCALE_LANGUAGE_MATCH; - case 2: - if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; - if (!referenceParams[1].equals(testedParams[1])) - return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; - if (3 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH; - return LOCALE_FULL_MATCH; - case 3: - if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; - if (!referenceParams[1].equals(testedParams[1])) - return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; - if (2 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER; - if (!referenceParams[2].equals(testedParams[2])) - return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER; - return LOCALE_FULL_MATCH; - } - // It should be impossible to come here - return LOCALE_NO_MATCH; - } - - /** - * Return a string that represents this match level, with better matches first. - * - * The strings are sorted in lexicographic order: a better match will always be less than - * a worse match when compared together. - */ - public static String getMatchLevelSortedString(int matchLevel) { - // This works because the match levels are 0~99 (actually 0~30) - // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel - return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel); - } - - /** - * Find out whether a match level should be considered a match. - * - * This method takes a match level as returned by the #getMatchLevel method, and returns whether - * it should be considered a match in the usual sense with standard Locale functions. - * - * @param level the match level, as returned by getMatchLevel. - * @return whether this is a match or not. - */ - public static boolean isMatch(int level) { - return LOCALE_MATCH <= level; - } - - private static final HashMap<String, Locale> sLocaleCache = new HashMap<>(); - - /** - * Creates a locale from a string specification. - */ - public static Locale constructLocaleFromString(final String localeStr) { - if (localeStr == null) { - return null; - } - synchronized (sLocaleCache) { - Locale retval = sLocaleCache.get(localeStr); - if (retval != null) { - return retval; - } - String[] localeParams = localeStr.split("_", 3); - if (localeParams.length == 1) { - retval = new Locale(localeParams[0]); - } else if (localeParams.length == 2) { - retval = new Locale(localeParams[0], localeParams[1]); - } else if (localeParams.length == 3) { - retval = new Locale(localeParams[0], localeParams[1], localeParams[2]); - } - if (retval != null) { - sLocaleCache.put(localeStr, retval); - } - return retval; - } - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/NgramContextUtils.java index 3cd63612c..c05ffd693 100644 --- a/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/NgramContextUtils.java @@ -16,18 +16,22 @@ package com.android.inputmethod.latin.utils; +import com.android.inputmethod.latin.NgramContext; +import com.android.inputmethod.latin.NgramContext.WordInfo; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; +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. } + private static final Pattern NEWLINE_REGEX = Pattern.compile("[\\r\\n]+"); private static final Pattern SPACE_REGEX = Pattern.compile("\\s+"); // Get context information from nth word before the cursor. n = 1 retrieves the words // immediately before the cursor, n = 2 retrieves the words before that, and so on. This splits @@ -43,7 +47,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 +55,18 @@ 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; - final String[] w = SPACE_REGEX.split(prev); - final WordInfo[] prevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + if (prev == null) return NgramContext.EMPTY_PREV_WORDS_INFO; + final String[] lines = NEWLINE_REGEX.split(prev); + if (lines.length == 0) { + return new NgramContext(WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO); + } + final String[] w = SPACE_REGEX.split(lines[lines.length - 1]); + final WordInfo[] prevWordsInfo = + new WordInfo[DecoderSpecificConstants.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 +77,37 @@ 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. + // If the word is empty, 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. + // If the word ends in a sentence terminator, the context is beginning-of-sentence. final char lastChar = focusedWord.charAt(length - 1); - if (spacingAndPunctuations.isSentenceSeparator(lastChar)) { - prevWordsInfo[i] = WordInfo.BEGINNING_OF_SENTENCE; + if (spacingAndPunctuations.isSentenceTerminator(lastChar)) { + 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/ScriptUtils.java b/java/src/com/android/inputmethod/latin/utils/ScriptUtils.java index 0e244666d..4679f7814 100644 --- a/java/src/com/android/inputmethod/latin/utils/ScriptUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ScriptUtils.java @@ -23,9 +23,10 @@ import java.util.TreeMap; * A class to help with handling different writing scripts. */ public class ScriptUtils { + // Used for hardware keyboards public static final int SCRIPT_UNKNOWN = -1; - // TODO: should we use ISO 15924 identifiers instead? + public static final int SCRIPT_ARABIC = 0; public static final int SCRIPT_ARMENIAN = 1; public static final int SCRIPT_BENGALI = 2; @@ -44,35 +45,31 @@ public class ScriptUtils { public static final int SCRIPT_TAMIL = 15; public static final int SCRIPT_TELUGU = 16; public static final int SCRIPT_THAI = 17; - public static final TreeMap<String, Integer> mSpellCheckerLanguageToScript; + + private static final TreeMap<String, Integer> mLanguageCodeToScriptCode; + static { - // List of the supported languages and their associated script. We won't check - // words written in another script than the selected script, because we know we - // don't have those in our dictionary so we will underline everything and we - // will never have any suggestions, so it makes no sense checking them, and this - // is done in {@link #shouldFilterOut}. Also, the script is used to choose which - // proximity to pass to the dictionary descent algorithm. - // IMPORTANT: this only contains languages - do not write countries in there. - // Only the language is searched from the map. - mSpellCheckerLanguageToScript = new TreeMap<>(); - mSpellCheckerLanguageToScript.put("cs", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("da", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("de", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("el", SCRIPT_GREEK); - mSpellCheckerLanguageToScript.put("en", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("es", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("fi", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("fr", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("hr", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("it", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("lt", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("lv", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("nb", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("nl", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("pt", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("sl", SCRIPT_LATIN); - mSpellCheckerLanguageToScript.put("ru", SCRIPT_CYRILLIC); + mLanguageCodeToScriptCode = new TreeMap<>(); + mLanguageCodeToScriptCode.put("", SCRIPT_LATIN); // default + mLanguageCodeToScriptCode.put("ar", SCRIPT_ARABIC); + mLanguageCodeToScriptCode.put("hy", SCRIPT_ARMENIAN); + mLanguageCodeToScriptCode.put("bn", SCRIPT_BENGALI); + mLanguageCodeToScriptCode.put("bg", SCRIPT_CYRILLIC); + mLanguageCodeToScriptCode.put("sr", SCRIPT_CYRILLIC); + mLanguageCodeToScriptCode.put("ru", SCRIPT_CYRILLIC); + mLanguageCodeToScriptCode.put("ka", SCRIPT_GEORGIAN); + mLanguageCodeToScriptCode.put("el", SCRIPT_GREEK); + mLanguageCodeToScriptCode.put("he", SCRIPT_HEBREW); + mLanguageCodeToScriptCode.put("km", SCRIPT_KHMER); + mLanguageCodeToScriptCode.put("lo", SCRIPT_LAO); + mLanguageCodeToScriptCode.put("ml", SCRIPT_MALAYALAM); + mLanguageCodeToScriptCode.put("my", SCRIPT_MYANMAR); + mLanguageCodeToScriptCode.put("si", SCRIPT_SINHALA); + mLanguageCodeToScriptCode.put("ta", SCRIPT_TAMIL); + mLanguageCodeToScriptCode.put("te", SCRIPT_TELUGU); + mLanguageCodeToScriptCode.put("th", SCRIPT_THAI); } + /* * Returns whether the code point is a letter that makes sense for the specified * locale for this spell checker. @@ -181,11 +178,17 @@ public class ScriptUtils { } } + /** + * @param locale spell checker locale + * @return internal Latin IME script code that maps to a language code + * {@see http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes} + */ public static int getScriptFromSpellCheckerLocale(final Locale locale) { - final Integer script = mSpellCheckerLanguageToScript.get(locale.getLanguage()); - if (null == script) { - throw new RuntimeException("We have been called with an unsupported language: \"" - + locale.getLanguage() + "\". Framework bug?"); + String language = locale.getLanguage(); + Integer script = mLanguageCodeToScriptCode.get(language); + if (script == null) { + // Default to Latin. + script = mLanguageCodeToScriptCode.get(""); } return script; } 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 79128dbd2..000000000 --- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java +++ /dev/null @@ -1,646 +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 toUpperCaseOfStringForLocale(s, true /* needsToUpperCase */, 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 toUpperCaseOfStringForLocale( - s.substring(0, cutoff), true /* needsToUpperCase */, locale) + s.substring(cutoff); - } - - public static String capitalizeFirstAndDowncaseRest(final String s, final Locale locale) { - if (s.length() <= 1) { - return toUpperCaseOfStringForLocale(s, true /* needsToUpperCase */, locale); - } - // TODO: fix the bugs below - // - 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); - final String titleCaseFirstLetter = toUpperCaseOfStringForLocale( - s.substring(0, cutoff), true /* needsToUpperCase */, locale); - return titleCaseFirstLetter + 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; - } - - private static final String LANGUAGE_GREEK = "el"; - - private static Locale getLocaleUsedForToTitleCase(final Locale locale) { - // In Greek locale {@link String#toUpperCase(Locale)} eliminates accents from its result. - // In order to get accented upper case letter, {@link Locale#ROOT} should be used. - if (LANGUAGE_GREEK.equals(locale.getLanguage())) { - return Locale.ROOT; - } - return locale; - } - - public static String toUpperCaseOfStringForLocale(final String text, - final boolean needsToUpperCase, final Locale locale) { - if (text == null || !needsToUpperCase) { - return text; - } - return text.toUpperCase(getLocaleUsedForToTitleCase(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..54a3fc39c 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,25 @@ 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.common.LocaleUtils; +import com.android.inputmethod.latin.common.StringUtils; -import java.util.Arrays; import java.util.HashMap; import java.util.Locale; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * 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"; @@ -47,11 +55,13 @@ public final class SubtypeLocaleUtils { private static volatile boolean sInitialized = false; private static final Object sInitializeLock = new Object(); private static Resources sResources; - private static String[] sPredefinedKeyboardLayoutSet; // Keyboard layout to its display name map. 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 +75,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 = @@ -89,7 +101,6 @@ public final class SubtypeLocaleUtils { sResources = res; final String[] predefinedLayoutSet = res.getStringArray(R.array.predefined_layouts); - sPredefinedKeyboardLayoutSet = predefinedLayoutSet; final String[] layoutDisplayNames = res.getStringArray( R.array.predefined_layout_display_names); for (int i = 0; i < predefinedLayoutSet.length; i++) { @@ -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++) { @@ -129,10 +149,6 @@ public final class SubtypeLocaleUtils { } } - public static String[] getPredefinedKeyboardLayoutSet() { - return sPredefinedKeyboardLayoutSet; - } - public static boolean isExceptionalLocale(final String localeString) { return sExceptionalLocaleToNameIdsMap.containsKey(localeString); } @@ -153,36 +169,58 @@ public final class SubtypeLocaleUtils { return nameId == null ? UNKNOWN_KEYBOARD_LAYOUT : nameId; } - private static Locale getDisplayLocaleOfSubtypeLocale(final String localeString) { + @Nonnull + public static Locale getDisplayLocaleOfSubtypeLocale(@Nonnull final String localeString) { if (NO_LANGUAGE.equals(localeString)) { return sResources.getConfiguration().locale; } + if (sExceptionalLocaleDisplayedInRootLocale.containsKey(localeString)) { + return Locale.ROOT; + } return LocaleUtils.constructLocaleFromString(localeString); } - public static String getSubtypeLocaleDisplayNameInSystemLocale(final String localeString) { + public static String getSubtypeLocaleDisplayNameInSystemLocale( + @Nonnull final String localeString) { final Locale displayLocale = sResources.getConfiguration().locale; return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale); } - public static String getSubtypeLocaleDisplayName(final String localeString) { + @Nonnull + public static String getSubtypeLocaleDisplayName(@Nonnull final String localeString) { final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString); return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale); } - public static String getSubtypeLanguageDisplayName(final String localeString) { - final Locale locale = LocaleUtils.constructLocaleFromString(localeString); + @Nonnull + public static String getSubtypeLanguageDisplayName(@Nonnull final String localeString) { final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString); - return getSubtypeLocaleDisplayNameInternal(locale.getLanguage(), displayLocale); + final String languageString; + if (sExceptionalLocaleDisplayedInRootLocale.containsKey(localeString)) { + languageString = localeString; + } else { + languageString = LocaleUtils.constructLocaleFromString(localeString).getLanguage(); + } + return getSubtypeLocaleDisplayNameInternal(languageString, displayLocale); } - private static String getSubtypeLocaleDisplayNameInternal(final String localeString, - final Locale displayLocale) { + @Nonnull + private static String getSubtypeLocaleDisplayNameInternal(@Nonnull final String localeString, + @Nonnull final Locale displayLocale) { if (NO_LANGUAGE.equals(localeString)) { // 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>() { @@ -193,8 +231,8 @@ public final class SubtypeLocaleUtils { }; displayName = getExceptionalName.runInLocale(sResources, displayLocale); } else { - final Locale locale = LocaleUtils.constructLocaleFromString(localeString); - displayName = locale.getDisplayName(displayLocale); + displayName = LocaleUtils.constructLocaleFromString(localeString) + .getDisplayName(displayLocale); } return StringUtils.capitalizeFirstCodePoint(displayName, displayLocale); } @@ -217,31 +255,36 @@ public final class SubtypeLocaleUtils { // en_US azerty T English (US) (AZERTY) exception // zz azerty T Alphabet (AZERTY) in system locale - private static String getReplacementString(final InputMethodSubtype subtype, - final Locale displayLocale) { + @Nonnull + private static String getReplacementString(@Nonnull final InputMethodSubtype subtype, + @Nonnull final Locale displayLocale) { 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) { + @Nonnull + public static String getSubtypeDisplayNameInSystemLocale( + @Nonnull final InputMethodSubtype subtype) { final Locale displayLocale = sResources.getConfiguration().locale; return getSubtypeDisplayNameInternal(subtype, displayLocale); } - public static String getSubtypeNameForLogging(final InputMethodSubtype subtype) { + @Nonnull + public static String getSubtypeNameForLogging(@Nullable final InputMethodSubtype subtype) { if (subtype == null) { return "<null subtype>"; } return getSubtypeLocale(subtype) + "/" + getKeyboardLayoutSetName(subtype); } - private static String getSubtypeDisplayNameInternal(final InputMethodSubtype subtype, - final Locale displayLocale) { + @Nonnull + private static String getSubtypeDisplayNameInternal(@Nonnull final InputMethodSubtype subtype, + @Nonnull 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,25 +307,25 @@ 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) { + @Nonnull + public static Locale getSubtypeLocale(@Nonnull final InputMethodSubtype subtype) { final String localeString = subtype.getLocale(); return LocaleUtils.constructLocaleFromString(localeString); } - public static String getKeyboardLayoutSetDisplayName(final InputMethodSubtype subtype) { + @Nonnull + public static String getKeyboardLayoutSetDisplayName( + @Nonnull final InputMethodSubtype subtype) { final String layoutName = getKeyboardLayoutSetName(subtype); return getKeyboardLayoutSetDisplayName(layoutName); } - public static String getKeyboardLayoutSetDisplayName(final String layoutName) { + @Nonnull + public static String getKeyboardLayoutSetDisplayName(@Nonnull final String layoutName) { return sKeyboardLayoutToDisplayNameMap.get(layoutName); } + @Nonnull public static String getKeyboardLayoutSetName(final InputMethodSubtype subtype) { String keyboardLayoutSet = subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET); if (keyboardLayoutSet == null) { @@ -302,27 +345,7 @@ public final class SubtypeLocaleUtils { return keyboardLayoutSet; } - // TODO: Get this information from the framework instead of maintaining here by ourselves. - // Sorted list of known Right-To-Left language codes. - private static final String[] SORTED_RTL_LANGUAGES = { - "ar", // Arabic - "fa", // Persian - "iw", // Hebrew - }; - static { - Arrays.sort(SORTED_RTL_LANGUAGES); - } - - public static boolean isRtlLanguage(final Locale locale) { - final String language = locale.getLanguage(); - 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..981355115 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,23 @@ 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; + public final boolean mFirstSuggestionExceedsConfidenceThreshold; 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, + final boolean firstSuggestionExceedsConfidenceThreshold) { + this(sSuggestedWordInfoComparator, capacity, isBeginningOfSentence, + firstSuggestionExceedsConfidenceThreshold); } - private SuggestionResults(final Locale locale, final Comparator<SuggestedWordInfo> comparator, - final int capacity, final boolean isBeginningOfSentence) { + private SuggestionResults(final Comparator<SuggestedWordInfo> comparator, final int capacity, + final boolean isBeginningOfSentence, + final boolean firstSuggestionExceedsConfidenceThreshold) { super(comparator); - mLocale = locale; mCapacity = capacity; if (ProductionFlags.INCLUDE_RAW_SUGGESTIONS) { mRawSuggestions = new ArrayList<>(); @@ -53,6 +53,7 @@ public final class SuggestionResults extends TreeSet<SuggestedWordInfo> { mRawSuggestions = null; } mIsBeginningOfSentence = isBeginningOfSentence; + mFirstSuggestionExceedsConfidenceThreshold = firstSuggestionExceedsConfidenceThreshold; } @Override @@ -70,8 +71,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..fc0a9cb6c --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/WordInputEventForPersonalization.java @@ -0,0 +1,106 @@ +/* + * 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.StringUtils; +import com.android.inputmethod.latin.define.DecoderSpecificConstants; +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 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[DecoderSpecificConstants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][]; + public final boolean[] mIsPrevWordBeginningOfSentenceArray = + new boolean[DecoderSpecificConstants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + // Time stamp in seconds. + public final int mTimestamp; + + @UsedForTesting + public WordInputEventForPersonalization(final CharSequence targetWord, + final NgramContext ngramContext, final int timestamp) { + mTargetWord = StringUtils.toCodePointArray(targetWord); + mPrevWordsCount = ngramContext.getPrevWordCount(); + ngramContext.outputToArray(mPrevWordArray, mIsPrevWordBeginningOfSentenceArray); + 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 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); + 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) { + if (locale == null) { + return null; + } + return new WordInputEventForPersonalization(targetWord, ngramContext, timestamp); + } +} |