aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
-rw-r--r--java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java20
-rw-r--r--java/src/com/android/inputmethod/latin/AssetFileAddress.java10
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java140
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java26
-rw-r--r--java/src/com/android/inputmethod/latin/Constants.java57
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java25
-rw-r--r--java/src/com/android/inputmethod/latin/Dictionary.java7
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java454
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFactory.java50
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryWriter.java26
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java481
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableDictionary.java894
-rw-r--r--java/src/com/android/inputmethod/latin/InputAttributes.java8
-rw-r--r--java/src/com/android/inputmethod/latin/InputView.java228
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java2080
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java252
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeSwitcher.java83
-rw-r--r--java/src/com/android/inputmethod/latin/Suggest.java188
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestedWords.java6
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java28
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java22
-rw-r--r--java/src/com/android/inputmethod/latin/UserBinaryDictionary.java25
-rw-r--r--java/src/com/android/inputmethod/latin/WordComposer.java47
-rw-r--r--java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java1744
-rw-r--r--java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java172
-rw-r--r--java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java54
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java48
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java113
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java62
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java233
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/DictDecoder.java11
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/DictUpdater.java54
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java492
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FormatSpec.java158
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java147
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/SparseTableContentReader.java126
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/SparseTableContentWriter.java93
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver2DictDecoder.java (renamed from java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java)20
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver2DictEncoder.java (renamed from java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java)12
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java82
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java341
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java457
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java59
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java169
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java190
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java50
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegistrar.java (renamed from java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java)13
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java84
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java104
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java37
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java26
-rw-r--r--java/src/com/android/inputmethod/latin/settings/DebugSettings.java5
-rw-r--r--java/src/com/android/inputmethod/latin/settings/Settings.java61
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SettingsFragment.java127
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SettingsValues.java96
-rw-r--r--java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java10
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java4
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java1
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java2
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java5
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java2
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java17
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java20
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java79
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java20
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java12
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java47
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java11
-rw-r--r--java/src/com/android/inputmethod/latin/utils/FileUtils.java54
-rw-r--r--java/src/com/android/inputmethod/latin/utils/JsonUtils.java103
-rw-r--r--java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java (renamed from java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java)20
-rw-r--r--java/src/com/android/inputmethod/latin/utils/LocaleUtils.java47
-rw-r--r--java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java1
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ResourceUtils.java8
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java9
-rw-r--r--java/src/com/android/inputmethod/latin/utils/StringUtils.java118
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java4
-rw-r--r--java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java14
-rw-r--r--java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java36
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UnigramProperty.java82
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java12
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java233
83 files changed, 5324 insertions, 6246 deletions
diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
index 463d09344..1aee22baf 100644
--- a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
@@ -21,21 +21,19 @@ import android.util.Log;
import com.android.inputmethod.latin.makedict.DictEncoder;
import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
-import com.android.inputmethod.latin.makedict.Ver3DictEncoder;
+import com.android.inputmethod.latin.makedict.Ver2DictEncoder;
import java.io.File;
import java.io.IOException;
import java.util.Map;
-// TODO: Quit extending Dictionary after implementing dynamic binary dictionary.
-abstract public class AbstractDictionaryWriter extends Dictionary {
+abstract public class AbstractDictionaryWriter {
/** Used for Log actions from this class */
private static final String TAG = AbstractDictionaryWriter.class.getSimpleName();
private final Context mContext;
- public AbstractDictionaryWriter(final Context context, final String dictType) {
- super(dictType);
+ public AbstractDictionaryWriter(final Context context) {
mContext = context;
}
@@ -55,20 +53,18 @@ abstract public class AbstractDictionaryWriter extends Dictionary {
// TODO: Remove lastModifiedTime after making binary dictionary support forgetting curve.
abstract public void addBigramWords(final String word0, final String word1,
- final int frequency, final boolean isValid,
- final long lastModifiedTime);
+ final int frequency, final boolean isValid, final long lastModifiedTime);
abstract public void removeBigramWords(final String word0, final String word1);
abstract protected void writeDictionary(final DictEncoder dictEncoder,
final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException;
- public void write(final String fileName, final Map<String, String> attributeMap) {
- final String tempFileName = fileName + ".temp";
- final File file = new File(mContext.getFilesDir(), fileName);
- final File tempFile = new File(mContext.getFilesDir(), tempFileName);
+ public void write(final File file, final Map<String, String> attributeMap) {
+ final String tempFilePath = file.getAbsolutePath() + ".temp";
+ final File tempFile = new File(tempFilePath);
try {
- final DictEncoder dictEncoder = new Ver3DictEncoder(tempFile);
+ final DictEncoder dictEncoder = new Ver2DictEncoder(tempFile);
writeDictionary(dictEncoder, attributeMap);
tempFile.renameTo(file);
} catch (IOException e) {
diff --git a/java/src/com/android/inputmethod/latin/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
index 875192554..fd6c24dfe 100644
--- a/java/src/com/android/inputmethod/latin/AssetFileAddress.java
+++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
@@ -16,6 +16,8 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.latin.utils.FileUtils;
+
import java.io.File;
/**
@@ -52,4 +54,12 @@ public final class AssetFileAddress {
if (!f.isFile()) return null;
return new AssetFileAddress(filename, offset, length);
}
+
+ public boolean pointsToPhysicalFile() {
+ return 0 == mOffset;
+ }
+
+ public void deleteUnderlyingFile() {
+ FileUtils.deleteRecursively(new File(mFilename));
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index fd296988e..95ac3e203 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -26,6 +26,7 @@ import com.android.inputmethod.latin.settings.NativeSuggestOptions;
import com.android.inputmethod.latin.utils.CollectionUtils;
import com.android.inputmethod.latin.utils.JniUtils;
import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.utils.UnigramProperty;
import java.io.File;
import java.util.ArrayList;
@@ -57,6 +58,21 @@ public final class BinaryDictionary extends Dictionary {
@UsedForTesting
public static final String MAX_BIGRAM_COUNT_QUERY = "MAX_BIGRAM_COUNT";
+ public static final int NOT_A_VALID_TIMESTAMP = -1;
+
+ // Format to get unigram flags from native side via getUnigramPropertyNative().
+ private static final int FORMAT_UNIGRAM_PROPERTY_OUTPUT_FLAG_COUNT = 4;
+ private static final int FORMAT_UNIGRAM_PROPERTY_IS_NOT_A_WORD_INDEX = 0;
+ private static final int FORMAT_UNIGRAM_PROPERTY_IS_BLACKLISTED_INDEX = 1;
+ private static final int FORMAT_UNIGRAM_PROPERTY_HAS_BIGRAMS_INDEX = 2;
+ private static final int FORMAT_UNIGRAM_PROPERTY_HAS_SHORTCUTS_INDEX = 3;
+
+ // Format to get unigram historical info from native side via getUnigramPropertyNative().
+ private static final int FORMAT_UNIGRAM_PROPERTY_OUTPUT_HISTORICAL_INFO_COUNT = 3;
+ private static final int FORMAT_UNIGRAM_PROPERTY_TIMESTAMP_INDEX = 0;
+ private static final int FORMAT_UNIGRAM_PROPERTY_LEVEL_INDEX = 1;
+ private static final int FORMAT_UNIGRAM_PROPERTY_COUNT_INDEX = 2;
+
private long mNativeDict;
private final Locale mLocale;
private final long mDictSize;
@@ -123,8 +139,13 @@ public final class BinaryDictionary extends Dictionary {
private static native boolean needsToRunGCNative(long dict, boolean mindsBlockByGC);
private static native void flushWithGCNative(long dict, String filePath);
private static native void closeNative(long dict);
+ private static native int getFormatVersionNative(long dict);
private static native int getProbabilityNative(long dict, int[] word);
private static native int getBigramProbabilityNative(long dict, int[] word0, int[] word1);
+ private static native void getUnigramPropertyNative(long dict, int[] word,
+ int[] outCodePoints, boolean[] outFlags, int[] outProbability,
+ int[] outHistoricalInfo, ArrayList<int[]> outShortcutTargets,
+ ArrayList<Integer> outShortcutProbabilities);
private static native int getSuggestionsNative(long dict, long proximityInfo,
long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times,
int[] pointerIds, int[] inputCodePoints, int inputSize, int commitPoint,
@@ -133,10 +154,14 @@ public final class BinaryDictionary extends Dictionary {
int[] outputAutoCommitFirstWordConfidence);
private static native float calcNormalizedScoreNative(int[] before, int[] after, int score);
private static native int editDistanceNative(int[] before, int[] after);
- private static native void addUnigramWordNative(long dict, int[] word, int probability);
+ private static native void addUnigramWordNative(long dict, int[] word, int probability,
+ int[] shortcutTarget, int shortcutProbability, boolean isNotAWord,
+ boolean isBlacklisted, int timestamp);
private static native void addBigramWordsNative(long dict, int[] word0, int[] word1,
- int probability);
+ int probability, int timestamp);
private static native void removeBigramWordsNative(long dict, int[] word0, int[] word1);
+ private static native int addMultipleDictionaryEntriesNative(long dict,
+ LanguageModelParam[] languageModelParams, int startIndex);
private static native int calculateProbabilityNative(long dict, int unigramProbability,
int bigramProbability);
private static native String getPropertyNative(long dict, String query);
@@ -235,6 +260,10 @@ public final class BinaryDictionary extends Dictionary {
return mNativeDict != 0;
}
+ public int getFormatVersion() {
+ return getFormatVersionNative(mNativeDict);
+ }
+
public static float calcNormalizedScore(final String before, final String after,
final int score) {
return calcNormalizedScoreNative(StringUtils.toCodePointArray(before),
@@ -274,23 +303,55 @@ public final class BinaryDictionary extends Dictionary {
return getBigramProbabilityNative(mNativeDict, codePoints0, codePoints1);
}
- // Add a unigram entry to binary dictionary in native code.
- public void addUnigramWord(final String word, final int probability) {
+ @UsedForTesting
+ public UnigramProperty getUnigramProperty(final String word) {
+ if (TextUtils.isEmpty(word)) {
+ return null;
+ }
+ final int[] codePoints = StringUtils.toCodePointArray(word);
+ final int[] outCodePoints = new int[MAX_WORD_LENGTH];
+ final boolean[] outFlags = new boolean[FORMAT_UNIGRAM_PROPERTY_OUTPUT_FLAG_COUNT];
+ final int[] outProbability = new int[1];
+ final int[] outHistoricalInfo =
+ new int[FORMAT_UNIGRAM_PROPERTY_OUTPUT_HISTORICAL_INFO_COUNT];
+ final ArrayList<int[]> outShortcutTargets = CollectionUtils.newArrayList();
+ final ArrayList<Integer> outShortcutProbabilities = CollectionUtils.newArrayList();
+ getUnigramPropertyNative(mNativeDict, codePoints, outCodePoints, outFlags, outProbability,
+ outHistoricalInfo, outShortcutTargets, outShortcutProbabilities);
+ return new UnigramProperty(codePoints,
+ outFlags[FORMAT_UNIGRAM_PROPERTY_IS_NOT_A_WORD_INDEX],
+ outFlags[FORMAT_UNIGRAM_PROPERTY_IS_BLACKLISTED_INDEX],
+ outFlags[FORMAT_UNIGRAM_PROPERTY_HAS_BIGRAMS_INDEX],
+ outFlags[FORMAT_UNIGRAM_PROPERTY_HAS_SHORTCUTS_INDEX], outProbability[0],
+ outHistoricalInfo[FORMAT_UNIGRAM_PROPERTY_TIMESTAMP_INDEX],
+ outHistoricalInfo[FORMAT_UNIGRAM_PROPERTY_LEVEL_INDEX],
+ outHistoricalInfo[FORMAT_UNIGRAM_PROPERTY_COUNT_INDEX],
+ outShortcutTargets, outShortcutProbabilities);
+ }
+
+ // Add a unigram entry to binary dictionary with unigram attributes in native code.
+ public void addUnigramWord(final String word, final int probability,
+ final String shortcutTarget, final int shortcutProbability, final boolean isNotAWord,
+ final boolean isBlacklisted, final int timestamp) {
if (TextUtils.isEmpty(word)) {
return;
}
final int[] codePoints = StringUtils.toCodePointArray(word);
- addUnigramWordNative(mNativeDict, codePoints, probability);
+ final int[] shortcutTargetCodePoints = (shortcutTarget != null) ?
+ StringUtils.toCodePointArray(shortcutTarget) : null;
+ addUnigramWordNative(mNativeDict, codePoints, probability, shortcutTargetCodePoints,
+ shortcutProbability, isNotAWord, isBlacklisted, timestamp);
}
- // Add a bigram entry to binary dictionary in native code.
- public void addBigramWords(final String word0, final String word1, final int probability) {
+ // Add a bigram entry to binary dictionary with timestamp in native code.
+ public void addBigramWords(final String word0, final String word1, final int probability,
+ final int timestamp) {
if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) {
return;
}
final int[] codePoints0 = StringUtils.toCodePointArray(word0);
final int[] codePoints1 = StringUtils.toCodePointArray(word1);
- addBigramWordsNative(mNativeDict, codePoints0, codePoints1, probability);
+ addBigramWordsNative(mNativeDict, codePoints0, codePoints1, probability, timestamp);
}
// Remove a bigram entry form binary dictionary in native code.
@@ -303,10 +364,71 @@ public final class BinaryDictionary extends Dictionary {
removeBigramWordsNative(mNativeDict, codePoints0, codePoints1);
}
+ public static class LanguageModelParam {
+ 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;
+ public final int mTimestamp;
+
+ // Constructor for unigram. TODO: support shortcuts
+ public LanguageModelParam(final String word, final int unigramProbability,
+ final int timestamp) {
+ mWord0 = null;
+ mWord1 = StringUtils.toCodePointArray(word);
+ mShortcutTarget = null;
+ mUnigramProbability = unigramProbability;
+ mBigramProbability = NOT_A_PROBABILITY;
+ mShortcutProbability = NOT_A_PROBABILITY;
+ mIsNotAWord = false;
+ mIsBlacklisted = false;
+ mTimestamp = timestamp;
+ }
+
+ // Constructor for unigram and bigram.
+ public LanguageModelParam(final String word0, final String word1,
+ final int unigramProbability, final int bigramProbability,
+ final int timestamp) {
+ mWord0 = StringUtils.toCodePointArray(word0);
+ mWord1 = StringUtils.toCodePointArray(word1);
+ mShortcutTarget = null;
+ mUnigramProbability = unigramProbability;
+ mBigramProbability = bigramProbability;
+ mShortcutProbability = NOT_A_PROBABILITY;
+ mIsNotAWord = false;
+ mIsBlacklisted = false;
+ mTimestamp = timestamp;
+ }
+ }
+
+ public void addMultipleDictionaryEntries(final LanguageModelParam[] languageModelParams) {
+ if (!isValidDictionary()) return;
+ int processedParamCount = 0;
+ while (processedParamCount < languageModelParams.length) {
+ if (needsToRunGC(true /* mindsBlockByGC */)) {
+ flushWithGC();
+ }
+ processedParamCount = addMultipleDictionaryEntriesNative(mNativeDict,
+ languageModelParams, processedParamCount);
+ if (processedParamCount <= 0) {
+ return;
+ }
+ }
+
+ }
+
private void reopen() {
close();
final File dictFile = new File(mDictFilePath);
- mNativeDict = openNative(dictFile.getAbsolutePath(), 0 /* startOffset */,
+ // WARNING: Because we pass 0 as the offset and file.length() as the length, this can
+ // only be called for actual files. Right now it's only called by the flush() family of
+ // functions, which require an updatable dictionary, so it's okay. But beware.
+ loadDictionary(dictFile.getAbsolutePath(), 0 /* startOffset */,
dictFile.length(), true /* isUpdatable */);
}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 722a82961..b4382bc2c 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -98,7 +98,7 @@ public final class BinaryDictionaryFileDumper {
* This creates a URI builder able to build a URI pointing to the dictionary
* pack content provider for a specific dictionary id.
*/
- private static Uri.Builder getProviderUriBuilder(final String path) {
+ public static Uri.Builder getProviderUriBuilder(final String path) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(DictionaryPackConstants.AUTHORITY).appendPath(path);
}
@@ -339,15 +339,25 @@ public final class BinaryDictionaryFileDumper {
Log.e(TAG, "Could not copy a word list. Will not be able to use it.");
// If we can't copy it we should warn the dictionary provider so that it can mark it
// as invalid.
- wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT,
- QUERY_PARAMETER_FAILURE);
+ reportBrokenFileToDictionaryProvider(providerClient, clientId, wordlistId);
+ }
+
+ public static boolean reportBrokenFileToDictionaryProvider(
+ final ContentProviderClient providerClient, final String clientId,
+ final String wordlistId) {
try {
+ final Uri.Builder wordListUriBuilder = getContentUriBuilderForType(clientId,
+ providerClient, QUERY_PATH_DATAFILE, wordlistId /* extraPath */);
+ wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT,
+ QUERY_PARAMETER_FAILURE);
if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) {
- Log.e(TAG, "In addition, we were unable to delete it.");
+ Log.e(TAG, "Unable to delete a word list.");
}
} catch (RemoteException e) {
- Log.e(TAG, "In addition, communication with the dictionary provider was cut", e);
+ Log.e(TAG, "Communication with the dictionary provider was cut", e);
+ return false;
}
+ return true;
}
// Ideally the two following methods should be merged, but AssetFileDescriptor does not
@@ -432,8 +442,9 @@ public final class BinaryDictionaryFileDumper {
// Actually copy the file
final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE];
- for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer))
+ for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer)) {
output.write(buffer, 0, readBytes);
+ }
input.close();
}
@@ -478,8 +489,7 @@ public final class BinaryDictionaryFileDumper {
* @param context the context for resources and providers.
* @param clientId the client ID to use.
*/
- public static void initializeClientRecordHelper(final Context context,
- final String clientId) {
+ public static void initializeClientRecordHelper(final Context context, final String clientId) {
try {
final ContentProviderClient client = context.getContentResolver().
acquireContentProviderClient(getProviderUriBuilder("").build());
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 9a9653094..00b54f593 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -70,38 +70,47 @@ public final class Constants {
public static final class ExtraValue {
/**
- * The subtype extra value used to indicate that the subtype keyboard layout is capable
- * for typing ASCII characters.
+ * 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 the subtype keyboard layout is capable
- * for typing EMOJI characters.
+ * 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 the subtype require network connection
- * to work.
+ * 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 subtype display name contains "%s"
- * for replacement mark and it should be replaced by this extra value.
+ * 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 that the subtype keyboard layout set name.
+ * 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 the subtype is additional subtype
+ * 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";
@@ -124,6 +133,8 @@ public final class Constants {
* {@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() {
@@ -132,7 +143,7 @@ public final class Constants {
}
public static final int NOT_A_CODE = -1;
-
+ public static final int NOT_A_CURSOR_POSITION = -1;
public static final int NOT_A_COORDINATE = -1;
public static final int SUGGESTION_STRIP_COORDINATE = -2;
public static final int SPELL_CHECKER_COORDINATE = -3;
@@ -145,6 +156,13 @@ public final class Constants {
// Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h
public static final int DICTIONARY_MAX_WORD_LENGTH = 48;
+ // 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 boolean isValidCoordinate(final int coordinate) {
// Detect {@link NOT_A_COORDINATE}, {@link SUGGESTION_STRIP_COORDINATE},
// and {@link SPELL_CHECKER_COORDINATE}.
@@ -230,8 +248,23 @@ public final class Constants {
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 47891c6b7..a787ef153 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -16,8 +16,6 @@
package com.android.inputmethod.latin;
-import com.android.inputmethod.latin.personalization.AccountUtils;
-
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
@@ -31,8 +29,11 @@ import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.util.Log;
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.personalization.AccountUtils;
import com.android.inputmethod.latin.utils.StringUtils;
+import java.io.File;
import java.util.List;
import java.util.Locale;
@@ -44,7 +45,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
private static final String TAG = ContactsBinaryDictionary.class.getSimpleName();
private static final String NAME = "contacts";
- private static boolean DEBUG = false;
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_DUMP = false;
/**
* Frequency for contacts information into the dictionary
@@ -71,8 +73,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
private final boolean mUseFirstLastBigrams;
public ContactsBinaryDictionary(final Context context, final Locale locale) {
- super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS,
- false /* isUpdatable */);
+ super(context, getDictNameWithLocale(NAME, locale), locale,
+ Dictionary.TYPE_CONTACTS, false /* isUpdatable */);
mLocale = locale;
mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
registerObserver(context);
@@ -82,6 +84,12 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
loadDictionary();
}
+ // Dummy constructor for tests.
+ @UsedForTesting
+ public ContactsBinaryDictionary(final Context context, final Locale locale, final File file) {
+ this(context, locale);
+ }
+
private synchronized void registerObserver(final Context context) {
// Perform a managed query. The Activity will handle closing and requerying the cursor
// when needed.
@@ -168,6 +176,10 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
if (isValidName(name)) {
addName(name);
++count;
+ } else {
+ if (DEBUG_DUMP) {
+ Log.d(TAG, "Invalid name: " + name);
+ }
}
cursor.moveToNext();
}
@@ -204,6 +216,9 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
if (Character.isLetter(name.codePointAt(i))) {
int end = getWordEndPosition(name, len, i);
String word = name.substring(i, end);
+ if (DEBUG_DUMP) {
+ Log.d(TAG, "addName word = " + word);
+ }
i = end - 1;
// Don't add single letter words, possibly confuses
// capitalization of i.
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index fa79f5af7..e04524843 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -52,13 +52,10 @@ public abstract class Dictionary {
public static final String TYPE_CONTACTS = "contacts";
// User dictionary, the system-managed one.
public static final String TYPE_USER = "user";
- // User history dictionary internal to LatinIME. This assumes bigram prediction for now.
+ // User history dictionary internal to LatinIME.
public static final String TYPE_USER_HISTORY = "history";
- // Personalization binary dictionary internal to LatinIME.
+ // Personalization dictionary.
public static final String TYPE_PERSONALIZATION = "personalization";
- // Personalization prediction dictionary internal to LatinIME's Java code.
- public static final String TYPE_PERSONALIZATION_PREDICTION_IN_JAVA =
- "personalization_prediction_in_java";
public final String mDictType;
public Dictionary(final String dictType) {
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
new file mode 100644
index 000000000..c9bcfe369
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
@@ -0,0 +1,454 @@
+/*
+ * 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.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
+import com.android.inputmethod.latin.personalization.PersonalizationHelper;
+import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
+import com.android.inputmethod.latin.settings.SettingsValues;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+// TODO: Consolidate dictionaries in native code.
+public class DictionaryFacilitatorForSuggest {
+ public static final String TAG = DictionaryFacilitatorForSuggest.class.getSimpleName();
+
+ private final Context mContext;
+ private final Locale mLocale;
+
+ private final ConcurrentHashMap<String, Dictionary> mDictionaries =
+ CollectionUtils.newConcurrentHashMap();
+ private HashSet<String> mDictionarySubsetForDebug = null;
+
+ private Dictionary mMainDictionary;
+ private ContactsBinaryDictionary mContactsDictionary;
+ private UserBinaryDictionary mUserDictionary;
+ private UserHistoryDictionary mUserHistoryDictionary;
+ private PersonalizationDictionary mPersonalizationDictionary;
+
+ @UsedForTesting
+ private boolean mIsCurrentlyWaitingForMainDictionary = false;
+
+ public interface DictionaryInitializationListener {
+ public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable);
+ }
+
+ /**
+ * Creates instance for initialization or when the locale is changed.
+ *
+ * @param context the context
+ * @param locale the locale
+ * @param settingsValues current settings values to control what dictionaries should be used
+ * @param listener the listener
+ * @param oldDictionaryFacilitator the instance having old dictionaries. This is null when the
+ * instance is initially created.
+ */
+ public DictionaryFacilitatorForSuggest(final Context context, final Locale locale,
+ final SettingsValues settingsValues, final DictionaryInitializationListener listener,
+ final DictionaryFacilitatorForSuggest oldDictionaryFacilitator) {
+ mContext = context;
+ mLocale = locale;
+ initForDebug(settingsValues);
+ reloadMainDict(context, locale, listener);
+ setUserDictionary(new UserBinaryDictionary(context, locale));
+ resetAdditionalDictionaries(oldDictionaryFacilitator, settingsValues);
+ }
+
+ /**
+ * Creates instance for when the settings values have been changed.
+ *
+ * @param settingsValues the new settings values
+ * @param oldDictionaryFacilitator the instance having old dictionaries. This must not be null.
+ */
+ //
+ public DictionaryFacilitatorForSuggest(final SettingsValues settingsValues,
+ final DictionaryFacilitatorForSuggest oldDictionaryFacilitator) {
+ mContext = oldDictionaryFacilitator.mContext;
+ mLocale = oldDictionaryFacilitator.mLocale;
+ initForDebug(settingsValues);
+ // Transfer main dictionary.
+ setMainDictionary(oldDictionaryFacilitator.mMainDictionary);
+ oldDictionaryFacilitator.removeDictionary(Dictionary.TYPE_MAIN);
+ // Transfer user dictionary.
+ setUserDictionary(oldDictionaryFacilitator.mUserDictionary);
+ oldDictionaryFacilitator.removeDictionary(Dictionary.TYPE_USER);
+ // Transfer or create additional dictionaries depending on the settings values.
+ resetAdditionalDictionaries(oldDictionaryFacilitator, settingsValues);
+ }
+
+ @UsedForTesting
+ public DictionaryFacilitatorForSuggest(final Context context, final Locale locale,
+ final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles) {
+ mContext = context;
+ mLocale = locale;
+ for (final String dictType : dictionaryTypes) {
+ if (dictType.equals(Dictionary.TYPE_MAIN)) {
+ final DictionaryCollection mainDictionary =
+ DictionaryFactory.createMainDictionaryFromManager(context, locale);
+ setMainDictionary(mainDictionary);
+ } else if (dictType.equals(Dictionary.TYPE_USER_HISTORY)) {
+ final UserHistoryDictionary userHistoryDictionary =
+ PersonalizationHelper.getUserHistoryDictionary(context, locale);
+ // Staring with an empty user history dictionary for testing.
+ // Testing program may populate this dictionary before actual testing.
+ userHistoryDictionary.reloadDictionaryIfRequired();
+ userHistoryDictionary.waitAllTasksForTests();
+ setUserHistoryDictionary(userHistoryDictionary);
+ } else if (dictType.equals(Dictionary.TYPE_PERSONALIZATION)) {
+ final PersonalizationDictionary personalizationDictionary =
+ PersonalizationHelper.getPersonalizationDictionary(context, locale);
+ // Staring with an empty personalization dictionary for testing.
+ // Testing program may populate this dictionary before actual testing.
+ personalizationDictionary.reloadDictionaryIfRequired();
+ personalizationDictionary.waitAllTasksForTests();
+ setPersonalizationDictionary(personalizationDictionary);
+ } else if (dictType.equals(Dictionary.TYPE_USER)) {
+ final File file = dictionaryFiles.get(dictType);
+ final UserBinaryDictionary userDictionary = new UserBinaryDictionary(
+ context, locale, file);
+ userDictionary.reloadDictionaryIfRequired();
+ userDictionary.waitAllTasksForTests();
+ setUserDictionary(userDictionary);
+ } else if (dictType.equals(Dictionary.TYPE_CONTACTS)) {
+ final File file = dictionaryFiles.get(dictType);
+ final ContactsBinaryDictionary contactsDictionary = new ContactsBinaryDictionary(
+ context, locale, file);
+ contactsDictionary.reloadDictionaryIfRequired();
+ contactsDictionary.waitAllTasksForTests();
+ setContactsDictionary(contactsDictionary);
+ } else {
+ throw new RuntimeException("Unknown dictionary type: " + dictType);
+ }
+ }
+ }
+
+ // initialize a debug flag for the personalization
+ private void initForDebug(final SettingsValues settingsValues) {
+ if (settingsValues.mUseOnlyPersonalizationDictionaryForDebug) {
+ mDictionarySubsetForDebug = new HashSet<String>();
+ mDictionarySubsetForDebug.add(Dictionary.TYPE_PERSONALIZATION);
+ }
+ }
+
+ public void close() {
+ final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet();
+ dictionaries.addAll(mDictionaries.values());
+ for (final Dictionary dictionary : dictionaries) {
+ dictionary.close();
+ }
+ }
+
+ public void reloadMainDict(final Context context, final Locale locale,
+ final DictionaryInitializationListener listener) {
+ mIsCurrentlyWaitingForMainDictionary = true;
+ mMainDictionary = null;
+ if (listener != null) {
+ listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+ }
+ new Thread("InitializeBinaryDictionary") {
+ @Override
+ public void run() {
+ final DictionaryCollection newMainDict =
+ DictionaryFactory.createMainDictionaryFromManager(context, locale);
+ setMainDictionary(newMainDict);
+ if (listener != null) {
+ listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+ }
+ mIsCurrentlyWaitingForMainDictionary = false;
+ }
+ }.start();
+ }
+
+ // The main dictionary could have been loaded asynchronously. Don't cache the return value
+ // of this method.
+ public boolean hasMainDictionary() {
+ return null != mMainDictionary && mMainDictionary.isInitialized();
+ }
+
+ @UsedForTesting
+ public boolean isCurrentlyWaitingForMainDictionary() {
+ return mIsCurrentlyWaitingForMainDictionary;
+ }
+
+ private void setMainDictionary(final Dictionary mainDictionary) {
+ mMainDictionary = mainDictionary;
+ addOrReplaceDictionary(Dictionary.TYPE_MAIN, mainDictionary);
+ }
+
+ /**
+ * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
+ * before the main dictionary, if set. This refers to the system-managed user dictionary.
+ */
+ private void setUserDictionary(final UserBinaryDictionary userDictionary) {
+ mUserDictionary = userDictionary;
+ addOrReplaceDictionary(Dictionary.TYPE_USER, userDictionary);
+ }
+
+ /**
+ * Sets an optional contacts dictionary resource to be loaded. It is also possible to remove
+ * the contacts dictionary by passing null to this method. In this case no contacts dictionary
+ * won't be used.
+ */
+ private void setContactsDictionary(final ContactsBinaryDictionary contactsDictionary) {
+ mContactsDictionary = contactsDictionary;
+ addOrReplaceDictionary(Dictionary.TYPE_CONTACTS, contactsDictionary);
+ }
+
+ private void setUserHistoryDictionary(final UserHistoryDictionary userHistoryDictionary) {
+ mUserHistoryDictionary = userHistoryDictionary;
+ addOrReplaceDictionary(Dictionary.TYPE_USER_HISTORY, userHistoryDictionary);
+ }
+
+ private void setPersonalizationDictionary(
+ final PersonalizationDictionary personalizationDictionary) {
+ mPersonalizationDictionary = personalizationDictionary;
+ addOrReplaceDictionary(Dictionary.TYPE_PERSONALIZATION, personalizationDictionary);
+ }
+
+ /**
+ * Reset dictionaries that can be turned off according to the user settings.
+ *
+ * @param oldDictionaryFacilitator the instance having old dictionaries
+ * @param settingsValues current SettingsValues
+ */
+ private void resetAdditionalDictionaries(
+ final DictionaryFacilitatorForSuggest oldDictionaryFacilitator,
+ final SettingsValues settingsValues) {
+ // Contacts dictionary
+ resetContactsDictionary(null != oldDictionaryFacilitator ?
+ oldDictionaryFacilitator.mContactsDictionary : null, settingsValues);
+ // User history dictionary & Personalization dictionary
+ resetPersonalizedDictionaries(oldDictionaryFacilitator, settingsValues);
+ }
+
+ /**
+ * Set the user history dictionary and personalization dictionary according to the user
+ * settings.
+ *
+ * @param oldDictionaryFacilitator the instance that has been used
+ * @param settingsValues current settingsValues
+ */
+ // TODO: Consolidate resetPersonalizedDictionaries() and resetContactsDictionary(). Call up the
+ // new method for each dictionary.
+ private void resetPersonalizedDictionaries(
+ final DictionaryFacilitatorForSuggest oldDictionaryFacilitator,
+ final SettingsValues settingsValues) {
+ final boolean shouldSetDictionaries = settingsValues.mUsePersonalizedDicts;
+
+ final UserHistoryDictionary oldUserHistoryDictionary = (null == oldDictionaryFacilitator) ?
+ null : oldDictionaryFacilitator.mUserHistoryDictionary;
+ final PersonalizationDictionary oldPersonalizationDictionary =
+ (null == oldDictionaryFacilitator) ? null :
+ oldDictionaryFacilitator.mPersonalizationDictionary;
+ final UserHistoryDictionary userHistoryDictionaryToUse;
+ final PersonalizationDictionary personalizationDictionaryToUse;
+ if (!shouldSetDictionaries) {
+ userHistoryDictionaryToUse = null;
+ personalizationDictionaryToUse = null;
+ } else {
+ if (null != oldUserHistoryDictionary
+ && oldUserHistoryDictionary.mLocale.equals(mLocale)) {
+ userHistoryDictionaryToUse = oldUserHistoryDictionary;
+ } else {
+ userHistoryDictionaryToUse =
+ PersonalizationHelper.getUserHistoryDictionary(mContext, mLocale);
+ }
+ if (null != oldPersonalizationDictionary
+ && oldPersonalizationDictionary.mLocale.equals(mLocale)) {
+ personalizationDictionaryToUse = oldPersonalizationDictionary;
+ } else {
+ personalizationDictionaryToUse =
+ PersonalizationHelper.getPersonalizationDictionary(mContext, mLocale);
+ }
+ }
+ setUserHistoryDictionary(userHistoryDictionaryToUse);
+ setPersonalizationDictionary(personalizationDictionaryToUse);
+ }
+
+ /**
+ * Set the contacts dictionary according to the user settings.
+ *
+ * This method takes an optional contacts dictionary to use when the locale hasn't changed
+ * since the contacts dictionary can be opened or closed as necessary depending on the settings.
+ *
+ * @param oldContactsDictionary an optional dictionary to use, or null
+ * @param settingsValues current settingsValues
+ */
+ private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary,
+ final SettingsValues settingsValues) {
+ final boolean shouldSetDictionary = settingsValues.mUseContactsDict;
+ final ContactsBinaryDictionary dictionaryToUse;
+ if (!shouldSetDictionary) {
+ // Make sure the dictionary is closed. If it is already closed, this is a no-op,
+ // so it's safe to call it anyways.
+ if (null != oldContactsDictionary) oldContactsDictionary.close();
+ dictionaryToUse = null;
+ } else {
+ if (null != oldContactsDictionary) {
+ if (!oldContactsDictionary.mLocale.equals(mLocale)) {
+ // If the locale has changed then recreate the contacts dictionary. This
+ // allows locale dependent rules for handling bigram name predictions.
+ oldContactsDictionary.close();
+ dictionaryToUse = new ContactsBinaryDictionary(mContext, mLocale);
+ } else {
+ // Make sure the old contacts dictionary is opened. If it is already open,
+ // this is a no-op, so it's safe to call it anyways.
+ oldContactsDictionary.reopen(mContext);
+ dictionaryToUse = oldContactsDictionary;
+ }
+ } else {
+ dictionaryToUse = new ContactsBinaryDictionary(mContext, mLocale);
+ }
+ }
+ setContactsDictionary(dictionaryToUse);
+ }
+
+ public boolean isUserDictionaryEnabled() {
+ if (mUserDictionary == null) {
+ return false;
+ }
+ return mUserDictionary.mEnabled;
+ }
+
+ public void addWordToUserDictionary(String word) {
+ if (mUserDictionary == null) {
+ return;
+ }
+ mUserDictionary.addWordToUserDictionary(word);
+ }
+
+ public void addToUserHistory(final WordComposer wordComposer, final String previousWord,
+ final String suggestion) {
+ if (mUserHistoryDictionary == null) {
+ return;
+ }
+ final String secondWord;
+ if (wordComposer.wasAutoCapitalized() && !wordComposer.isMostlyCaps()) {
+ secondWord = suggestion.toLowerCase(mLocale);
+ } else {
+ secondWord = suggestion;
+ }
+ // 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 int maxFreq = getMaxFrequency(suggestion);
+ if (maxFreq == 0) {
+ return;
+ }
+ final boolean isValid = maxFreq > 0;
+ final int timeStamp = (int)TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis()));
+ mUserHistoryDictionary.addToDictionary(previousWord, secondWord, isValid, timeStamp);
+ }
+
+ public void cancelAddingUserHistory(final String previousWord, final String committedWord) {
+ if (mUserHistoryDictionary != null) {
+ mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
+ }
+ }
+
+ // TODO: Revise the way to fusion suggestion results.
+ public void getSuggestions(final WordComposer composer,
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+ final int sessionId, final Set<SuggestedWordInfo> suggestionSet) {
+ for (final String key : mDictionaries.keySet()) {
+ final Dictionary dictionary = mDictionaries.get(key);
+ suggestionSet.addAll(dictionary.getSuggestionsWithSessionId(composer, prevWord,
+ proximityInfo, blockOffensiveWords, additionalFeaturesOptions, sessionId));
+ }
+ }
+
+ public boolean isValidMainDictWord(final String word) {
+ if (TextUtils.isEmpty(word) || !hasMainDictionary()) {
+ return false;
+ }
+ return mMainDictionary.isValidWord(word);
+ }
+
+ public boolean isValidWord(final String word, final boolean ignoreCase) {
+ if (TextUtils.isEmpty(word)) {
+ return false;
+ }
+ final String lowerCasedWord = word.toLowerCase(mLocale);
+ for (final String key : mDictionaries.keySet()) {
+ final Dictionary dictionary = mDictionaries.get(key);
+ // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
+ // managing to get null in here. Presumably the language is changing to a language with
+ // no main dictionary and the monkey manages to type a whole word before the thread
+ // that reads the dictionary is started or something?
+ // 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 getMaxFrequency(final String word) {
+ if (TextUtils.isEmpty(word)) {
+ return Dictionary.NOT_A_PROBABILITY;
+ }
+ int maxFreq = -1;
+ for (final String key : mDictionaries.keySet()) {
+ final Dictionary dictionary = mDictionaries.get(key);
+ if (null == dictionary) continue;
+ final int tempFreq = dictionary.getFrequency(word);
+ if (tempFreq >= maxFreq) {
+ maxFreq = tempFreq;
+ }
+ }
+ return maxFreq;
+ }
+
+ private void removeDictionary(final String key) {
+ mDictionaries.remove(key);
+ }
+
+ private void addOrReplaceDictionary(final String key, final Dictionary dict) {
+ if (mDictionarySubsetForDebug != null && !mDictionarySubsetForDebug.contains(key)) {
+ Log.w(TAG, "Ignore add " + key + " dictionary for debug.");
+ return;
+ }
+ final Dictionary oldDict;
+ if (dict == null) {
+ oldDict = mDictionaries.remove(key);
+ } else {
+ oldDict = mDictionaries.put(key, dict);
+ }
+ if (oldDict != null && dict != oldDict) {
+ oldDict.close();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index 828e54f14..e09c309ea 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -16,6 +16,7 @@
package com.android.inputmethod.latin;
+import android.content.ContentProviderClient;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
@@ -64,6 +65,10 @@ public final class DictionaryFactory {
useFullEditDistance, locale, Dictionary.TYPE_MAIN);
if (readOnlyBinaryDictionary.isValidDictionary()) {
dictList.add(readOnlyBinaryDictionary);
+ } else {
+ readOnlyBinaryDictionary.close();
+ // Prevent this dictionary to do any further harm.
+ killDictionary(context, f);
}
}
}
@@ -75,6 +80,51 @@ public final class DictionaryFactory {
}
/**
+ * Kills a dictionary so that it is never used again, if possible.
+ * @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) {
+ if (f.pointsToPhysicalFile()) {
+ f.deleteUnderlyingFile();
+ // Warn the dictionary provider if the dictionary came from there.
+ final ContentProviderClient providerClient;
+ try {
+ providerClient = context.getContentResolver().acquireContentProviderClient(
+ BinaryDictionaryFileDumper.getProviderUriBuilder("").build());
+ } catch (final SecurityException e) {
+ Log.e(TAG, "No permission to communicate with the dictionary provider", e);
+ return;
+ }
+ if (null == providerClient) {
+ Log.e(TAG, "Can't establish communication with the dictionary provider");
+ return;
+ }
+ 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);
+ }
+ }
+ }
+
+ /**
* 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
diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
index 3df2a2b63..89ef96d7f 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
@@ -18,8 +18,6 @@ package com.android.inputmethod.latin;
import android.content.Context;
-import com.android.inputmethod.keyboard.ProximityInfo;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.makedict.DictEncoder;
import com.android.inputmethod.latin.makedict.FormatSpec;
import com.android.inputmethod.latin.makedict.FusionDictionary;
@@ -37,14 +35,14 @@ import java.util.Map;
* An in memory dictionary for memorizing entries and writing a binary dictionary.
*/
public class DictionaryWriter extends AbstractDictionaryWriter {
- private static final int BINARY_DICT_VERSION = 3;
+ private static final int BINARY_DICT_VERSION = 2;
private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
- new FormatSpec.FormatOptions(BINARY_DICT_VERSION, true /* supportsDynamicUpdate */);
+ new FormatSpec.FormatOptions(BINARY_DICT_VERSION, false /* supportsDynamicUpdate */);
private FusionDictionary mFusionDictionary;
- public DictionaryWriter(final Context context, final String dictType) {
- super(context, dictType);
+ public DictionaryWriter(final Context context) {
+ super(context);
clear();
}
@@ -52,7 +50,7 @@ public class DictionaryWriter extends AbstractDictionaryWriter {
public void clear() {
final HashMap<String, String> attributes = CollectionUtils.newHashMap();
mFusionDictionary = new FusionDictionary(new PtNodeArray(),
- new FusionDictionary.DictionaryOptions(attributes, false, false));
+ new FusionDictionary.DictionaryOptions(attributes));
}
/**
@@ -92,18 +90,4 @@ public class DictionaryWriter extends AbstractDictionaryWriter {
}
dictEncoder.writeDictionary(mFusionDictionary, FORMAT_OPTIONS);
}
-
- @Override
- public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo,
- boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
- // This class doesn't support suggestion.
- return null;
- }
-
- @Override
- public boolean isValidWord(String word) {
- // This class doesn't support dictionary retrieval.
- return false;
- }
}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index eb8650e6f..f785835b8 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -17,23 +17,26 @@
package com.android.inputmethod.latin;
import android.content.Context;
-import android.os.SystemClock;
import android.util.Log;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.BinaryDictionary.LanguageModelParam;
import com.android.inputmethod.latin.makedict.FormatSpec;
-import com.android.inputmethod.latin.personalization.DynamicPersonalizationDictionaryWriter;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.utils.AsyncResultHolder;
import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.FileUtils;
import com.android.inputmethod.latin.utils.PrioritizedSerialExecutor;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -52,10 +55,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
/** Whether to print debug output to log */
private static boolean DEBUG = false;
-
- // TODO: Remove.
- /** Whether to call binary dictionary dynamically updating methods. */
- public static boolean ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE = true;
+ private static final boolean DBG_STRESS_TEST = false;
private static final int TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS = 100;
@@ -64,22 +64,19 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
protected static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
- private static final int DICTIONARY_FORMAT_VERSION = 3;
-
- private static final String SUPPORTS_DYNAMIC_UPDATE =
- FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE;
+ private static final int DICTIONARY_FORMAT_VERSION = FormatSpec.VERSION4;
/**
* A static map of update controllers, each of which records the time of accesses to a single
* binary dictionary file and tracks whether the file is regenerating. The key for this map is
- * the filename and the value is the shared dictionary time recorder associated with that
- * filename.
+ * the dictionary name and the value is the shared dictionary time recorder associated with
+ * that dictionary name.
*/
private static final ConcurrentHashMap<String, DictionaryUpdateController>
- sFilenameDictionaryUpdateControllerMap = CollectionUtils.newConcurrentHashMap();
+ sDictNameDictionaryUpdateControllerMap = CollectionUtils.newConcurrentHashMap();
private static final ConcurrentHashMap<String, PrioritizedSerialExecutor>
- sFilenameExecutorMap = CollectionUtils.newConcurrentHashMap();
+ sDictNameExecutorMap = CollectionUtils.newConcurrentHashMap();
/** The application context. */
protected final Context mContext;
@@ -95,18 +92,24 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
protected AbstractDictionaryWriter mDictionaryWriter;
/**
- * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple
- * dictionary instances with the same filename is supported, with access controlled by
- * DictionaryTimeRecorder.
+ * The name of this dictionary, used as a part of the filename for storing the binary
+ * dictionary. Multiple dictionary instances with the same name is supported, with access
+ * controlled by DictionaryUpdateController.
*/
- private final String mFilename;
+ private final String mDictName;
+
+ /** Dictionary locale */
+ private final Locale mLocale;
/** Whether to support dynamically updating the dictionary */
private final boolean mIsUpdatable;
+ /** Dictionary file */
+ private final File mDictFile;
+
// TODO: remove, once dynamic operations is serialized
/** Controls updating the shared binary dictionary file across multiple instances. */
- private final DictionaryUpdateController mFilenameDictionaryUpdateController;
+ private final DictionaryUpdateController mDictNameDictionaryUpdateController;
// TODO: remove, once dynamic operations is serialized
/** Controls updating the local binary dictionary for this instance. */
@@ -132,45 +135,70 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
protected abstract boolean hasContentChanged();
+ protected boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) {
+ // This class is using format 2 because it's used by the User and Contacts dictionary
+ // only, which right now use format 2 (dicts using format 4 use Decaying*, which overrides
+ // this method).
+ // TODO: Migrate these dicts to ver4 format, and remove this function.
+ return formatVersion == FormatSpec.VERSION2;
+ }
+
+ public boolean isValidDictionary() {
+ return mBinaryDictionary.isValidDictionary();
+ }
+
+ private File getDictFile() {
+ return mDictFile;
+ }
+
/**
- * Gets the dictionary update controller for the given filename.
+ * Gets the dictionary update controller for the given dictionary name.
*/
private static DictionaryUpdateController getDictionaryUpdateController(
- String filename) {
- DictionaryUpdateController recorder = sFilenameDictionaryUpdateControllerMap.get(filename);
+ final String dictName) {
+ DictionaryUpdateController recorder = sDictNameDictionaryUpdateControllerMap.get(dictName);
if (recorder == null) {
- synchronized(sFilenameDictionaryUpdateControllerMap) {
+ synchronized(sDictNameDictionaryUpdateControllerMap) {
recorder = new DictionaryUpdateController();
- sFilenameDictionaryUpdateControllerMap.put(filename, recorder);
+ sDictNameDictionaryUpdateControllerMap.put(dictName, recorder);
}
}
return recorder;
}
/**
- * Gets the executor for the given filename.
+ * Gets the executor for the given dictionary name.
*/
- private static PrioritizedSerialExecutor getExecutor(final String filename) {
- PrioritizedSerialExecutor executor = sFilenameExecutorMap.get(filename);
+ private static PrioritizedSerialExecutor getExecutor(final String dictName) {
+ PrioritizedSerialExecutor executor = sDictNameExecutorMap.get(dictName);
if (executor == null) {
- synchronized(sFilenameExecutorMap) {
+ synchronized(sDictNameExecutorMap) {
executor = new PrioritizedSerialExecutor();
- sFilenameExecutorMap.put(filename, executor);
+ sDictNameExecutorMap.put(dictName, executor);
}
}
return executor;
}
+ /**
+ * Shutdowns all executors and removes all executors from the executor map for testing.
+ */
+ @UsedForTesting
+ public static void shutdownAllExecutors() {
+ synchronized(sDictNameExecutorMap) {
+ for (final PrioritizedSerialExecutor executor : sDictNameExecutorMap.values()) {
+ executor.shutdown();
+ sDictNameExecutorMap.remove(executor);
+ }
+ }
+ }
+
private static AbstractDictionaryWriter getDictionaryWriter(final Context context,
- final String dictType, final boolean isDynamicPersonalizationDictionary) {
+ final boolean isDynamicPersonalizationDictionary) {
if (isDynamicPersonalizationDictionary) {
- if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
- return null;
- } else {
- return new DynamicPersonalizationDictionaryWriter(context, dictType);
- }
+ return null;
} else {
- return new DictionaryWriter(context, dictType);
+ return new DictionaryWriter(context);
}
}
@@ -178,26 +206,37 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* Creates a new expandable binary dictionary.
*
* @param context The application context of the parent.
- * @param filename The filename for this binary dictionary. Multiple dictionaries with the same
- * filename is supported.
+ * @param dictName The name of the dictionary. Multiple instances with the same
+ * name is supported.
+ * @param locale the dictionary locale.
* @param dictType the dictionary type, as a human-readable string
* @param isUpdatable whether to support dynamically updating the dictionary. Please note that
* dynamic dictionary has negative effects on memory space and computation time.
*/
- public ExpandableBinaryDictionary(final Context context, final String filename,
- final String dictType, final boolean isUpdatable) {
+ public ExpandableBinaryDictionary(final Context context, final String dictName,
+ final Locale locale, final String dictType, final boolean isUpdatable) {
+ this(context, dictName, locale, dictType, isUpdatable,
+ new File(context.getFilesDir(), dictName + DICT_FILE_EXTENSION));
+ }
+
+ // Creates an instance that uses a given dictionary file.
+ public ExpandableBinaryDictionary(final Context context, final String dictName,
+ final Locale locale, final String dictType, final boolean isUpdatable,
+ final File dictFile) {
super(dictType);
- mFilename = filename;
+ mDictName = dictName;
mContext = context;
+ mLocale = locale;
mIsUpdatable = isUpdatable;
+ mDictFile = dictFile;
mBinaryDictionary = null;
- mFilenameDictionaryUpdateController = getDictionaryUpdateController(filename);
+ mDictNameDictionaryUpdateController = getDictionaryUpdateController(dictName);
// Currently, only dynamic personalization dictionary is updatable.
- mDictionaryWriter = getDictionaryWriter(context, dictType, isUpdatable);
+ mDictionaryWriter = getDictionaryWriter(context, isUpdatable);
}
- protected static String getFilenameWithLocale(final String name, final String localeStr) {
- return name + "." + localeStr + DICT_FILE_EXTENSION;
+ protected static String getDictNameWithLocale(final String name, final Locale locale) {
+ return name + "." + locale.toString();
}
/**
@@ -205,23 +244,20 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
@Override
public void close() {
- getExecutor(mFilename).execute(new Runnable() {
+ getExecutor(mDictName).execute(new Runnable() {
@Override
public void run() {
if (mBinaryDictionary!= null) {
mBinaryDictionary.close();
mBinaryDictionary = null;
}
- if (mDictionaryWriter != null) {
- mDictionaryWriter.close();
- }
}
});
}
protected void closeBinaryDictionary() {
// Ensure that no other threads are accessing the local binary dictionary.
- getExecutor(mFilename).execute(new Runnable() {
+ getExecutor(mDictName).execute(new Runnable() {
@Override
public void run() {
if (mBinaryDictionary != null) {
@@ -234,19 +270,23 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
protected Map<String, String> getHeaderAttributeMap() {
HashMap<String, String> attributeMap = new HashMap<String, String>();
- attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE,
- SUPPORTS_DYNAMIC_UPDATE);
- attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFilename);
+ attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mDictName);
+ attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale.toString());
+ attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE,
+ String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
return attributeMap;
}
protected void clear() {
- getExecutor(mFilename).execute(new Runnable() {
+ getExecutor(mDictName).execute(new Runnable() {
@Override
public void run() {
- if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE && mDictionaryWriter == null) {
+ if (mDictionaryWriter == null) {
mBinaryDictionary.close();
- final File file = new File(mContext.getFilesDir(), mFilename);
+ final File file = getDictFile();
+ if (file.exists() && !FileUtils.deleteRecursively(file)) {
+ Log.e(TAG, "Can't remove a file: " + file.getName());
+ }
BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
mBinaryDictionary = new BinaryDictionary(
@@ -286,8 +326,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* Check whether GC is needed and run GC if required.
*/
protected void runGCIfRequired(final boolean mindsBlockByGC) {
- if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) return;
- getExecutor(mFilename).execute(new Runnable() {
+ getExecutor(mDictName).execute(new Runnable() {
@Override
public void run() {
runGCIfRequiredInternalLocked(mindsBlockByGC);
@@ -296,18 +335,17 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
private void runGCIfRequiredInternalLocked(final boolean mindsBlockByGC) {
- if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) return;
// Calls to needsToRunGC() need to be serialized.
if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) {
- if (setIsRegeneratingIfNotRegenerating()) {
+ if (setProcessingLargeTaskIfNot()) {
// Run GC after currently existing time sensitive operations.
- getExecutor(mFilename).executePrioritized(new Runnable() {
+ getExecutor(mDictName).executePrioritized(new Runnable() {
@Override
public void run() {
try {
mBinaryDictionary.flushWithGC();
} finally {
- mFilenameDictionaryUpdateController.mIsRegenerating.set(false);
+ mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false);
}
}
});
@@ -318,23 +356,19 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
/**
* Dynamically adds a word unigram to the dictionary. May overwrite an existing entry.
*/
- protected void addWordDynamically(final String word, final String shortcutTarget,
- final int frequency, final int shortcutFreq, final boolean isNotAWord) {
+ protected void addWordDynamically(final String word, final int frequency,
+ final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord,
+ final boolean isBlacklisted, final int timestamp) {
if (!mIsUpdatable) {
- Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename);
+ Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mDictName);
return;
}
- getExecutor(mFilename).execute(new Runnable() {
+ getExecutor(mDictName).execute(new Runnable() {
@Override
public void run() {
- if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
- runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
- mBinaryDictionary.addUnigramWord(word, frequency);
- } else {
- // TODO: Remove.
- mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, shortcutFreq,
- isNotAWord);
- }
+ runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
+ mBinaryDictionary.addUnigramWord(word, frequency, shortcutTarget, shortcutFreq,
+ isNotAWord, isBlacklisted, timestamp);
}
});
}
@@ -343,23 +377,17 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* Dynamically adds a word bigram in the dictionary. May overwrite an existing entry.
*/
protected void addBigramDynamically(final String word0, final String word1,
- final int frequency, final boolean isValid) {
+ final int frequency, final int timestamp) {
if (!mIsUpdatable) {
Log.w(TAG, "addBigramDynamically is called for non-updatable dictionary: "
- + mFilename);
+ + mDictName);
return;
}
- getExecutor(mFilename).execute(new Runnable() {
+ getExecutor(mDictName).execute(new Runnable() {
@Override
public void run() {
- if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
- runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
- mBinaryDictionary.addBigramWords(word0, word1, frequency);
- } else {
- // TODO: Remove.
- mDictionaryWriter.addBigramWords(word0, word1, frequency, isValid,
- 0 /* lastTouchedTime */);
- }
+ runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
+ mBinaryDictionary.addBigramWords(word0, word1, frequency, timestamp);
}
});
}
@@ -370,18 +398,48 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
protected void removeBigramDynamically(final String word0, final String word1) {
if (!mIsUpdatable) {
Log.w(TAG, "removeBigramDynamically is called for non-updatable dictionary: "
- + mFilename);
+ + mDictName);
return;
}
- getExecutor(mFilename).execute(new Runnable() {
+ getExecutor(mDictName).execute(new Runnable() {
@Override
public void run() {
- if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
- runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
- mBinaryDictionary.removeBigramWords(word0, word1);
- } else {
- // TODO: Remove.
- mDictionaryWriter.removeBigramWords(word0, word1);
+ runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
+ mBinaryDictionary.removeBigramWords(word0, word1);
+ }
+ });
+ }
+
+ public interface AddMultipleDictionaryEntriesCallback {
+ public void onFinished();
+ }
+
+ /**
+ * Dynamically add multiple entries to the dictionary.
+ */
+ protected void addMultipleDictionaryEntriesDynamically(
+ final ArrayList<LanguageModelParam> languageModelParams,
+ final AddMultipleDictionaryEntriesCallback callback) {
+ if (!mIsUpdatable) {
+ Log.w(TAG, "addMultipleDictionaryEntriesDynamically is called for non-updatable " +
+ "dictionary: " + mDictName);
+ return;
+ }
+ getExecutor(mDictName).execute(new Runnable() {
+ @Override
+ public void run() {
+ final boolean locked = setProcessingLargeTaskIfNot();
+ try {
+ mBinaryDictionary.addMultipleDictionaryEntries(
+ languageModelParams.toArray(
+ new LanguageModelParam[languageModelParams.size()]));
+ } finally {
+ if (callback != null) {
+ callback.onFinished();
+ }
+ if (locked) {
+ mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false);
+ }
}
}
});
@@ -393,49 +451,23 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
final int sessionId) {
reloadDictionaryIfRequired();
- if (isRegenerating()) {
+ if (processingLargeTask()) {
return null;
}
- final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
final AsyncResultHolder<ArrayList<SuggestedWordInfo>> holder =
new AsyncResultHolder<ArrayList<SuggestedWordInfo>>();
- getExecutor(mFilename).executePrioritized(new Runnable() {
+ getExecutor(mDictName).executePrioritized(new Runnable() {
@Override
public void run() {
- if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
- if (mBinaryDictionary == null) {
- holder.set(null);
- return;
- }
- final ArrayList<SuggestedWordInfo> binarySuggestion =
- mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord,
- proximityInfo, blockOffensiveWords, additionalFeaturesOptions,
- sessionId);
- holder.set(binarySuggestion);
- } else {
- final ArrayList<SuggestedWordInfo> inMemDictSuggestion =
- composer.isBatchMode() ? null :
- mDictionaryWriter.getSuggestionsWithSessionId(composer,
- prevWord, proximityInfo, blockOffensiveWords,
- additionalFeaturesOptions, sessionId);
- // TODO: Remove checking mIsUpdatable and use native suggestion.
- if (mBinaryDictionary != null && !mIsUpdatable) {
- final ArrayList<SuggestedWordInfo> binarySuggestion =
- mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord,
- proximityInfo, blockOffensiveWords,
- additionalFeaturesOptions, sessionId);
- if (inMemDictSuggestion == null) {
- holder.set(binarySuggestion);
- } else if (binarySuggestion == null) {
- holder.set(inMemDictSuggestion);
- } else {
- binarySuggestion.addAll(inMemDictSuggestion);
- holder.set(binarySuggestion);
- }
- } else {
- holder.set(inMemDictSuggestion);
- }
+ if (mBinaryDictionary == null) {
+ holder.set(null);
+ return;
}
+ final ArrayList<SuggestedWordInfo> binarySuggestion =
+ mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord,
+ proximityInfo, blockOffensiveWords, additionalFeaturesOptions,
+ sessionId);
+ holder.set(binarySuggestion);
}
});
return holder.get(null, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
@@ -456,11 +488,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
protected boolean isValidWordInner(final String word) {
- if (isRegenerating()) {
+ if (processingLargeTask()) {
return false;
}
final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>();
- getExecutor(mFilename).executePrioritized(new Runnable() {
+ getExecutor(mDictName).executePrioritized(new Runnable() {
@Override
public void run() {
holder.set(isValidWordLocked(word));
@@ -484,7 +516,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* dictionary exists, this method will generate one.
*/
protected void loadDictionary() {
- mPerInstanceDictionaryUpdateController.mLastUpdateRequestTime = SystemClock.uptimeMillis();
+ mPerInstanceDictionaryUpdateController.mLastUpdateRequestTime = System.currentTimeMillis();
reloadDictionaryIfRequired();
}
@@ -494,12 +526,22 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
private void loadBinaryDictionary() {
if (DEBUG) {
- Log.d(TAG, "Loading binary dictionary: " + mFilename + " request="
- + mFilenameDictionaryUpdateController.mLastUpdateRequestTime + " update="
- + mFilenameDictionaryUpdateController.mLastUpdateTime);
+ Log.d(TAG, "Loading binary dictionary: " + mDictName + " request="
+ + mDictNameDictionaryUpdateController.mLastUpdateRequestTime + " update="
+ + mDictNameDictionaryUpdateController.mLastUpdateTime);
+ }
+ if (DBG_STRESS_TEST) {
+ // Test if this class does not cause problems when it takes long time to load binary
+ // dictionary.
+ try {
+ Log.w(TAG, "Start stress in loading: " + mDictName);
+ Thread.sleep(15000);
+ Log.w(TAG, "End stress in loading");
+ } catch (InterruptedException e) {
+ }
}
- final File file = new File(mContext.getFilesDir(), mFilename);
+ final File file = getDictFile();
final String filename = file.getAbsolutePath();
final long length = file.length();
@@ -511,7 +553,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
// swapping in the new one.
// TODO: Ensure multi-thread assignment of mBinaryDictionary.
final BinaryDictionary oldBinaryDictionary = mBinaryDictionary;
- getExecutor(mFilename).executePrioritized(new Runnable() {
+ getExecutor(mDictName).executePrioritized(new Runnable() {
@Override
public void run() {
mBinaryDictionary = newBinaryDictionary;
@@ -533,29 +575,31 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
private void writeBinaryDictionary() {
if (DEBUG) {
- Log.d(TAG, "Generating binary dictionary: " + mFilename + " request="
- + mFilenameDictionaryUpdateController.mLastUpdateRequestTime + " update="
- + mFilenameDictionaryUpdateController.mLastUpdateTime);
+ Log.d(TAG, "Generating binary dictionary: " + mDictName + " request="
+ + mDictNameDictionaryUpdateController.mLastUpdateRequestTime + " update="
+ + mDictNameDictionaryUpdateController.mLastUpdateTime);
}
if (needsToReloadBeforeWriting()) {
mDictionaryWriter.clear();
loadDictionaryAsync();
- mDictionaryWriter.write(mFilename, getHeaderAttributeMap());
+ mDictionaryWriter.write(getDictFile(), getHeaderAttributeMap());
} else {
- if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
- if (mBinaryDictionary == null || !mBinaryDictionary.isValidDictionary()) {
- final File file = new File(mContext.getFilesDir(), mFilename);
- BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
- DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
- } else {
- if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
- mBinaryDictionary.flushWithGC();
- } else {
- mBinaryDictionary.flush();
- }
+ if (mBinaryDictionary == null || !isValidDictionary()
+ // TODO: remove the check below
+ || !matchesExpectedBinaryDictFormatVersionForThisType(
+ mBinaryDictionary.getFormatVersion())) {
+ final File file = getDictFile();
+ if (file.exists() && !FileUtils.deleteRecursively(file)) {
+ Log.e(TAG, "Can't remove a file: " + file.getName());
}
+ BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
+ DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
} else {
- mDictionaryWriter.write(mFilename, getHeaderAttributeMap());
+ if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
+ mBinaryDictionary.flushWithGC();
+ } else {
+ mBinaryDictionary.flush();
+ }
}
}
}
@@ -568,12 +612,12 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* the current binary dictionary from file.
*/
protected void setRequiresReload(final boolean requiresRebuild) {
- final long time = SystemClock.uptimeMillis();
+ final long time = System.currentTimeMillis();
mPerInstanceDictionaryUpdateController.mLastUpdateRequestTime = time;
- mFilenameDictionaryUpdateController.mLastUpdateRequestTime = time;
+ mDictNameDictionaryUpdateController.mLastUpdateRequestTime = time;
if (DEBUG) {
- Log.d(TAG, "Reload request: " + mFilename + ": request=" + time + " update="
- + mFilenameDictionaryUpdateController.mLastUpdateTime);
+ Log.d(TAG, "Reload request: " + mDictName + ": request=" + time + " update="
+ + mDictNameDictionaryUpdateController.mLastUpdateTime);
}
}
@@ -582,7 +626,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
public final void reloadDictionaryIfRequired() {
if (!isReloadRequired()) return;
- if (setIsRegeneratingIfNotRegenerating()) {
+ if (setProcessingLargeTaskIfNot()) {
reloadDictionary();
}
}
@@ -594,13 +638,14 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
return mBinaryDictionary == null || mPerInstanceDictionaryUpdateController.isOutOfDate();
}
- private boolean isRegenerating() {
- return mFilenameDictionaryUpdateController.mIsRegenerating.get();
+ private boolean processingLargeTask() {
+ return mDictNameDictionaryUpdateController.mProcessingLargeTask.get();
}
- // Returns whether the dictionary can be regenerated.
- private boolean setIsRegeneratingIfNotRegenerating() {
- return mFilenameDictionaryUpdateController.mIsRegenerating.compareAndSet(
+ // Returns whether the dictionary is being used for a large task. If true, we should not use
+ // this dictionary for latency sensitive operations.
+ private boolean setProcessingLargeTaskIfNot() {
+ return mDictNameDictionaryUpdateController.mProcessingLargeTask.compareAndSet(
false /* expect */ , true /* update */);
}
@@ -611,13 +656,13 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
private final void reloadDictionary() {
// Ensure that only one thread attempts to read or write to the shared binary dictionary
// file at the same time.
- getExecutor(mFilename).execute(new Runnable() {
+ getExecutor(mDictName).execute(new Runnable() {
@Override
public void run() {
try {
- final long time = SystemClock.uptimeMillis();
+ final long time = System.currentTimeMillis();
final boolean dictionaryFileExists = dictionaryFileExists();
- if (mFilenameDictionaryUpdateController.isOutOfDate()
+ if (mDictNameDictionaryUpdateController.isOutOfDate()
|| !dictionaryFileExists) {
// If the shared dictionary file does not exist or is out of date, the
// first instance that acquires the lock will generate a new one.
@@ -626,31 +671,44 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
// rebuild the binary dictionary. Empty dictionaries are supported (in
// the case where loadDictionaryAsync() adds nothing) in order to
// provide a uniform framework.
- mFilenameDictionaryUpdateController.mLastUpdateTime = time;
+ mDictNameDictionaryUpdateController.mLastUpdateTime = time;
writeBinaryDictionary();
loadBinaryDictionary();
} else {
// If not, the reload request was unnecessary so revert
// LastUpdateRequestTime to LastUpdateTime.
- mFilenameDictionaryUpdateController.mLastUpdateRequestTime =
- mFilenameDictionaryUpdateController.mLastUpdateTime;
+ mDictNameDictionaryUpdateController.mLastUpdateRequestTime =
+ mDictNameDictionaryUpdateController.mLastUpdateTime;
}
} else if (mBinaryDictionary == null ||
mPerInstanceDictionaryUpdateController.mLastUpdateTime
- < mFilenameDictionaryUpdateController.mLastUpdateTime) {
+ < mDictNameDictionaryUpdateController.mLastUpdateTime) {
// Otherwise, if the local dictionary is older than the shared dictionary,
// load the shared dictionary.
loadBinaryDictionary();
}
- if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) {
- // Binary dictionary is not valid. Regenerate the dictionary file.
- mFilenameDictionaryUpdateController.mLastUpdateTime = time;
- writeBinaryDictionary();
- loadBinaryDictionary();
- }
- mPerInstanceDictionaryUpdateController.mLastUpdateTime = time;
+ // If we just loaded the binary dictionary, then mBinaryDictionary is not
+ // up-to-date yet so it's useless to test it right away. Schedule the check
+ // for right after it's loaded instead.
+ getExecutor(mDictName).executePrioritized(new Runnable() {
+ @Override
+ public void run() {
+ if (mBinaryDictionary != null && !(isValidDictionary()
+ // TODO: remove the check below
+ && matchesExpectedBinaryDictFormatVersionForThisType(
+ mBinaryDictionary.getFormatVersion()))) {
+ // Binary dictionary or its format version is not valid. Regenerate
+ // the dictionary file. writeBinaryDictionary will remove the
+ // existing files if appropriate.
+ mDictNameDictionaryUpdateController.mLastUpdateTime = time;
+ writeBinaryDictionary();
+ loadBinaryDictionary();
+ }
+ mPerInstanceDictionaryUpdateController.mLastUpdateTime = time;
+ }
+ });
} finally {
- mFilenameDictionaryUpdateController.mIsRegenerating.set(false);
+ mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false);
}
}
});
@@ -658,28 +716,13 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
// TODO: cache the file's existence so that we avoid doing a disk access each time.
private boolean dictionaryFileExists() {
- final File file = new File(mContext.getFilesDir(), mFilename);
- return file.exists();
- }
-
- /**
- * Load the dictionary to memory.
- */
- protected void asyncLoadDictionaryToMemory() {
- getExecutor(mFilename).executePrioritized(new Runnable() {
- @Override
- public void run() {
- if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
- loadDictionaryAsync();
- }
- }
- });
+ return getDictFile().exists();
}
/**
* Generate binary dictionary using DictionaryWriter.
*/
- protected void asyncFlashAllBinaryDictionary() {
+ protected void asyncFlushBinaryDictionary() {
final Runnable newTask = new Runnable() {
@Override
public void run() {
@@ -687,37 +730,32 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
};
final Runnable oldTask = mUnfinishedFlushingTask.getAndSet(newTask);
- getExecutor(mFilename).replaceAndExecute(oldTask, newTask);
+ getExecutor(mDictName).replaceAndExecute(oldTask, newTask);
}
/**
- * For tracking whether the dictionary is out of date and the dictionary is regenerating.
- * Can be shared across multiple dictionary instances that access the same filename.
+ * For tracking whether the dictionary is out of date and the dictionary is used in a large
+ * task. Can be shared across multiple dictionary instances that access the same filename.
*/
private static class DictionaryUpdateController {
public volatile long mLastUpdateTime = 0;
public volatile long mLastUpdateRequestTime = 0;
- public volatile AtomicBoolean mIsRegenerating = new AtomicBoolean();
+ public volatile AtomicBoolean mProcessingLargeTask = new AtomicBoolean();
public boolean isOutOfDate() {
return (mLastUpdateRequestTime > mLastUpdateTime);
}
}
- // TODO: Implement native binary methods once the dynamic dictionary implementation is done.
+ // TODO: Implement BinaryDictionary.isInDictionary().
@UsedForTesting
public boolean isInDictionaryForTests(final String word) {
final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>();
- getExecutor(mFilename).executePrioritized(new Runnable() {
+ getExecutor(mDictName).executePrioritized(new Runnable() {
@Override
public void run() {
if (mDictType == Dictionary.TYPE_USER_HISTORY) {
- if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
- holder.set(mBinaryDictionary.isValidWord(word));
- } else {
- holder.set(((DynamicPersonalizationDictionaryWriter) mDictionaryWriter)
- .isInBigramListForTests(word));
- }
+ holder.set(mBinaryDictionary.isValidWord(word));
}
}
});
@@ -725,12 +763,33 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
@UsedForTesting
- public void shutdownExecutorForTests() {
- getExecutor(mFilename).shutdown();
+ public void waitAllTasksForTests() {
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ getExecutor(mDictName).execute(new Runnable() {
+ @Override
+ public void run() {
+ countDownLatch.countDown();
+ }
+ });
+ try {
+ countDownLatch.await();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Interrupted while waiting for finishing dictionary operations.", e);
+ }
}
@UsedForTesting
- public boolean isTerminatedForTests() {
- return getExecutor(mFilename).isTerminated();
+ protected void runAfterGcForDebug(final Runnable r) {
+ getExecutor(mDictName).executePrioritized(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mBinaryDictionary.flushWithGC();
+ r.run();
+ } finally {
+ mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false);
+ }
+ }
+ });
}
}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
deleted file mode 100644
index 95c9bcab9..000000000
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ /dev/null
@@ -1,894 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.text.TextUtils;
-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.utils.CollectionUtils;
-import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
-
-import java.util.ArrayList;
-import java.util.LinkedList;
-
-/**
- * Class for an in-memory dictionary that can grow dynamically and can
- * be searched for suggestions and valid words.
- */
-// TODO: Remove after binary dictionary supports dynamic update.
-public class ExpandableDictionary extends Dictionary {
- private static final String TAG = ExpandableDictionary.class.getSimpleName();
- /**
- * The weight to give to a word if it's length is the same as the number of typed characters.
- */
- private static final int FULL_WORD_SCORE_MULTIPLIER = 2;
-
- private char[] mWordBuilder = new char[Constants.DICTIONARY_MAX_WORD_LENGTH];
- private int mMaxDepth;
- private int mInputLength;
-
- private static final class Node {
- char mCode;
- int mFrequency;
- boolean mTerminal;
- Node mParent;
- NodeArray mChildren;
- ArrayList<char[]> mShortcutTargets;
- boolean mShortcutOnly;
- LinkedList<NextWord> mNGrams; // Supports ngram
- }
-
- private static final class NodeArray {
- Node[] mData;
- int mLength = 0;
- private static final int INCREMENT = 2;
-
- NodeArray() {
- mData = new Node[INCREMENT];
- }
-
- void add(final Node n) {
- if (mLength + 1 > mData.length) {
- Node[] tempData = new Node[mLength + INCREMENT];
- if (mLength > 0) {
- System.arraycopy(mData, 0, tempData, 0, mLength);
- }
- mData = tempData;
- }
- mData[mLength++] = n;
- }
- }
-
- public interface NextWord {
- public Node getWordNode();
- public int getFrequency();
- public ForgettingCurveParams getFcParams();
- public int notifyTypedAgainAndGetFrequency();
- }
-
- private static final class NextStaticWord implements NextWord {
- public final Node mWord;
- private final int mFrequency;
- public NextStaticWord(Node word, int frequency) {
- mWord = word;
- mFrequency = frequency;
- }
-
- @Override
- public Node getWordNode() {
- return mWord;
- }
-
- @Override
- public int getFrequency() {
- return mFrequency;
- }
-
- @Override
- public ForgettingCurveParams getFcParams() {
- return null;
- }
-
- @Override
- public int notifyTypedAgainAndGetFrequency() {
- return mFrequency;
- }
- }
-
- private static final class NextHistoryWord implements NextWord {
- public final Node mWord;
- public final ForgettingCurveParams mFcp;
-
- public NextHistoryWord(Node word, ForgettingCurveParams fcp) {
- mWord = word;
- mFcp = fcp;
- }
-
- @Override
- public Node getWordNode() {
- return mWord;
- }
-
- @Override
- public int getFrequency() {
- return mFcp.getFrequency();
- }
-
- @Override
- public ForgettingCurveParams getFcParams() {
- return mFcp;
- }
-
- @Override
- public int notifyTypedAgainAndGetFrequency() {
- return mFcp.notifyTypedAgainAndGetFrequency();
- }
- }
-
- private NodeArray mRoots;
-
- private int[][] mCodes;
-
- public ExpandableDictionary(final String dictType) {
- super(dictType);
- clearDictionary();
- mCodes = new int[Constants.DICTIONARY_MAX_WORD_LENGTH][];
- }
-
- public int getMaxWordLength() {
- return Constants.DICTIONARY_MAX_WORD_LENGTH;
- }
-
- /**
- * Add a word with an optional shortcut to the dictionary.
- * @param word The word to add.
- * @param shortcutTarget A shortcut target for this word, or null if none.
- * @param frequency The frequency for this unigram.
- * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored
- * if shortcutTarget is null.
- */
- public void addWord(final String word, final String shortcutTarget, final int frequency,
- final int shortcutFreq) {
- if (word.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH) {
- return;
- }
- addWordRec(mRoots, word, 0, shortcutTarget, frequency, shortcutFreq, null);
- }
-
- /**
- * Add a word, recursively searching for its correct place in the trie tree.
- * @param children The node to recursively search for addition. Initially, the root of the tree.
- * @param word The word to add.
- * @param depth The current depth in the tree.
- * @param shortcutTarget A shortcut target for this word, or null if none.
- * @param frequency The frequency for this unigram.
- * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored
- * if shortcutTarget is null.
- * @param parentNode The parent node, for up linking. Initially null, as the root has no parent.
- */
- private void addWordRec(final NodeArray children, final String word, final int depth,
- final String shortcutTarget, final int frequency, final int shortcutFreq,
- final Node parentNode) {
- final int wordLength = word.length();
- if (wordLength <= depth) return;
- final char c = word.charAt(depth);
- // Does children have the current character?
- final int childrenLength = children.mLength;
- Node childNode = null;
- for (int i = 0; i < childrenLength; i++) {
- final Node node = children.mData[i];
- if (node.mCode == c) {
- childNode = node;
- break;
- }
- }
- final boolean isShortcutOnly = (null != shortcutTarget);
- if (childNode == null) {
- childNode = new Node();
- childNode.mCode = c;
- childNode.mParent = parentNode;
- childNode.mShortcutOnly = isShortcutOnly;
- children.add(childNode);
- }
- if (wordLength == depth + 1) {
- // Terminate this word
- childNode.mTerminal = true;
- if (isShortcutOnly) {
- if (null == childNode.mShortcutTargets) {
- childNode.mShortcutTargets = CollectionUtils.newArrayList();
- }
- childNode.mShortcutTargets.add(shortcutTarget.toCharArray());
- } else {
- childNode.mShortcutOnly = false;
- }
- childNode.mFrequency = Math.max(frequency, childNode.mFrequency);
- if (childNode.mFrequency > 255) childNode.mFrequency = 255;
- return;
- }
- if (childNode.mChildren == null) {
- childNode.mChildren = new NodeArray();
- }
- addWordRec(childNode.mChildren, word, depth + 1, shortcutTarget, frequency, shortcutFreq,
- childNode);
- }
-
- @Override
- public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
- if (composer.size() > 1) {
- if (composer.size() >= Constants.DICTIONARY_MAX_WORD_LENGTH) {
- return null;
- }
- final ArrayList<SuggestedWordInfo> suggestions =
- getWordsInner(composer, prevWord, proximityInfo);
- return suggestions;
- } else {
- if (TextUtils.isEmpty(prevWord)) return null;
- final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
- runBigramReverseLookUp(prevWord, suggestions);
- return suggestions;
- }
- }
-
- private ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes,
- final String prevWordForBigrams, final ProximityInfo proximityInfo) {
- final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
- mInputLength = codes.size();
- if (mCodes.length < mInputLength) mCodes = new int[mInputLength][];
- final InputPointers ips = codes.getInputPointers();
- final int[] xCoordinates = ips.getXCoordinates();
- final int[] yCoordinates = ips.getYCoordinates();
- // Cache the codes so that we don't have to lookup an array list
- for (int i = 0; i < mInputLength; i++) {
- // TODO: Calculate proximity info here.
- if (mCodes[i] == null || mCodes[i].length < 1) {
- mCodes[i] = new int[ProximityInfo.MAX_PROXIMITY_CHARS_SIZE];
- }
- final int x = xCoordinates != null && i < xCoordinates.length ?
- xCoordinates[i] : Constants.NOT_A_COORDINATE;
- final int y = xCoordinates != null && i < yCoordinates.length ?
- yCoordinates[i] : Constants.NOT_A_COORDINATE;
- proximityInfo.fillArrayWithNearestKeyCodes(x, y, codes.getCodeAt(i), mCodes[i]);
- }
- mMaxDepth = mInputLength * 3;
- getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, suggestions);
- for (int i = 0; i < mInputLength; i++) {
- getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, suggestions);
- }
- return suggestions;
- }
-
- @Override
- public synchronized boolean isValidWord(final String word) {
- final Node node = searchNode(mRoots, word, 0, word.length());
- // If node is null, we didn't find the word, so it's not valid.
- // If node.mShortcutOnly is true, then it exists as a shortcut but not as a word,
- // so that means it's not a valid word.
- // If node.mShortcutOnly is false, then it exists as a word (it may also exist as
- // a shortcut, but this does not matter), so it's a valid word.
- return (node == null) ? false : !node.mShortcutOnly;
- }
-
- public boolean removeBigram(final String word0, final String word1) {
- // Refer to addOrSetBigram() about word1.toLowerCase()
- final Node firstWord = searchWord(mRoots, word0.toLowerCase(), 0, null);
- final Node secondWord = searchWord(mRoots, word1, 0, null);
- LinkedList<NextWord> bigrams = firstWord.mNGrams;
- NextWord bigramNode = null;
- if (bigrams == null || bigrams.size() == 0) {
- return false;
- } else {
- for (NextWord nw : bigrams) {
- if (nw.getWordNode() == secondWord) {
- bigramNode = nw;
- break;
- }
- }
- }
- if (bigramNode == null) {
- return false;
- }
- return bigrams.remove(bigramNode);
- }
-
- /**
- * Returns the word's frequency or -1 if not found
- */
- @UsedForTesting
- public int getWordFrequency(final String word) {
- // Case-sensitive search
- final Node node = searchNode(mRoots, word, 0, word.length());
- return (node == null) ? -1 : node.mFrequency;
- }
-
- public NextWord getBigramWord(final String word0, final String word1) {
- // Refer to addOrSetBigram() about word0.toLowerCase()
- final Node firstWord = searchWord(mRoots, word0.toLowerCase(), 0, null);
- final Node secondWord = searchWord(mRoots, word1, 0, null);
- LinkedList<NextWord> bigrams = firstWord.mNGrams;
- if (bigrams == null || bigrams.size() == 0) {
- return null;
- } else {
- for (NextWord nw : bigrams) {
- if (nw.getWordNode() == secondWord) {
- return nw;
- }
- }
- }
- return null;
- }
-
- private static int computeSkippedWordFinalFreq(final int freq, final int snr,
- final int inputLength) {
- // The computation itself makes sense for >= 2, but the == 2 case returns 0
- // anyway so we may as well test against 3 instead and return the constant
- if (inputLength >= 3) {
- return (freq * snr * (inputLength - 2)) / (inputLength - 1);
- } else {
- return 0;
- }
- }
-
- /**
- * Helper method to add a word and its shortcuts.
- *
- * @param node the terminal node
- * @param word the word to insert, as an array of code points
- * @param depth the depth of the node in the tree
- * @param finalFreq the frequency for this word
- * @param suggestions the suggestion collection to add the suggestions to
- * @return whether there is still space for more words.
- */
- private boolean addWordAndShortcutsFromNode(final Node node, final char[] word, final int depth,
- final int finalFreq, final ArrayList<SuggestedWordInfo> suggestions) {
- if (finalFreq > 0 && !node.mShortcutOnly) {
- // Use KIND_CORRECTION always. This dictionary does not really have a notion of
- // COMPLETION against CORRECTION; we could artificially add one by looking at
- // the respective size of the typed word and the suggestion if it matters sometime
- // in the future.
- suggestions.add(new SuggestedWordInfo(new String(word, 0, depth + 1), finalFreq,
- SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */,
- SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
- SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
- if (suggestions.size() >= Suggest.MAX_SUGGESTIONS) return false;
- }
- if (null != node.mShortcutTargets) {
- final int length = node.mShortcutTargets.size();
- for (int shortcutIndex = 0; shortcutIndex < length; ++shortcutIndex) {
- final char[] shortcut = node.mShortcutTargets.get(shortcutIndex);
- suggestions.add(new SuggestedWordInfo(new String(shortcut, 0, shortcut.length),
- finalFreq, SuggestedWordInfo.KIND_SHORTCUT, this /* sourceDict */,
- SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
- SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
- if (suggestions.size() > Suggest.MAX_SUGGESTIONS) return false;
- }
- }
- return true;
- }
-
- /**
- * Recursively traverse the tree for words that match the input. Input consists of
- * a list of arrays. Each item in the list is one input character position. An input
- * character is actually an array of multiple possible candidates. This function is not
- * optimized for speed, assuming that the user dictionary will only be a few hundred words in
- * size.
- * @param roots node whose children have to be search for matches
- * @param codes the input character codes
- * @param word the word being composed as a possible match
- * @param depth the depth of traversal - the length of the word being composed thus far
- * @param completion whether the traversal is now in completion mode - meaning that we've
- * exhausted the input and we're looking for all possible suffixes.
- * @param snr current weight of the word being formed
- * @param inputIndex position in the input characters. This can be off from the depth in
- * case we skip over some punctuations such as apostrophe in the traversal. That is, if you type
- * "wouldve", it could be matching "would've", so the depth will be one more than the
- * inputIndex
- * @param suggestions the list in which to add suggestions
- */
- // TODO: Share this routine with the native code for BinaryDictionary
- private void getWordsRec(final NodeArray roots, final WordComposer codes, final char[] word,
- final int depth, final boolean completion, final int snr, final int inputIndex,
- final int skipPos, final ArrayList<SuggestedWordInfo> suggestions) {
- final int count = roots.mLength;
- final int codeSize = mInputLength;
- // Optimization: Prune out words that are too long compared to how much was typed.
- if (depth > mMaxDepth) {
- return;
- }
- final int[] currentChars;
- if (codeSize <= inputIndex) {
- currentChars = null;
- } else {
- currentChars = mCodes[inputIndex];
- }
-
- for (int i = 0; i < count; i++) {
- final Node node = roots.mData[i];
- final char c = node.mCode;
- final char lowerC = toLowerCase(c);
- final boolean terminal = node.mTerminal;
- final NodeArray children = node.mChildren;
- final int freq = node.mFrequency;
- if (completion || currentChars == null) {
- word[depth] = c;
- if (terminal) {
- final int finalFreq;
- if (skipPos < 0) {
- finalFreq = freq * snr;
- } else {
- finalFreq = computeSkippedWordFinalFreq(freq, snr, mInputLength);
- }
- if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, suggestions)) {
- // No space left in the queue, bail out
- return;
- }
- }
- if (children != null) {
- getWordsRec(children, codes, word, depth + 1, true, snr, inputIndex,
- skipPos, suggestions);
- }
- } else if ((c == Constants.CODE_SINGLE_QUOTE
- && currentChars[0] != Constants.CODE_SINGLE_QUOTE) || depth == skipPos) {
- // Skip the ' and continue deeper
- word[depth] = c;
- if (children != null) {
- getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
- skipPos, suggestions);
- }
- } else {
- // Don't use alternatives if we're looking for missing characters
- final int alternativesSize = skipPos >= 0 ? 1 : currentChars.length;
- for (int j = 0; j < alternativesSize; j++) {
- final int addedAttenuation = (j > 0 ? 1 : 2);
- final int currentChar = currentChars[j];
- if (currentChar == Constants.NOT_A_CODE) {
- break;
- }
- if (currentChar == lowerC || currentChar == c) {
- word[depth] = c;
-
- if (codeSize == inputIndex + 1) {
- if (terminal) {
- final int finalFreq;
- if (skipPos < 0) {
- finalFreq = freq * snr * addedAttenuation
- * FULL_WORD_SCORE_MULTIPLIER;
- } else {
- finalFreq = computeSkippedWordFinalFreq(freq,
- snr * addedAttenuation, mInputLength);
- }
- if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq,
- suggestions)) {
- // No space left in the queue, bail out
- return;
- }
- }
- if (children != null) {
- getWordsRec(children, codes, word, depth + 1,
- true, snr * addedAttenuation, inputIndex + 1,
- skipPos, suggestions);
- }
- } else if (children != null) {
- getWordsRec(children, codes, word, depth + 1,
- false, snr * addedAttenuation, inputIndex + 1,
- skipPos, suggestions);
- }
- }
- }
- }
- }
- }
-
- public int setBigramAndGetFrequency(final String word0, final String word1,
- final int frequency) {
- return setBigramAndGetFrequency(word0, word1, frequency, null /* unused */);
- }
-
- public int setBigramAndGetFrequency(final String word0, final String word1,
- final ForgettingCurveParams fcp) {
- return setBigramAndGetFrequency(word0, word1, 0 /* unused */, fcp);
- }
-
- /**
- * Adds bigrams to the in-memory trie structure that is being used to retrieve any word
- * @param word0 the first word of this bigram
- * @param word1 the second word of this bigram
- * @param frequency frequency for this bigram
- * @param fcp an instance of ForgettingCurveParams to use for decay policy
- * @return returns the final bigram frequency
- */
- private int setBigramAndGetFrequency(final String word0, final String word1,
- final int frequency, final ForgettingCurveParams fcp) {
- if (TextUtils.isEmpty(word0)) {
- Log.e(TAG, "Invalid bigram previous word: " + word0);
- return frequency;
- }
- // We don't want results to be different according to case of the looked up left hand side
- // word. We do want however to return the correct case for the right hand side.
- // So we want to squash the case of the left hand side, and preserve that of the right
- // hand side word.
- final String word0Lower = word0.toLowerCase();
- if (TextUtils.isEmpty(word0Lower) || TextUtils.isEmpty(word1)) {
- Log.e(TAG, "Invalid bigram pair: " + word0 + ", " + word0Lower + ", " + word1);
- return frequency;
- }
- final Node firstWord = searchWord(mRoots, word0Lower, 0, null);
- final Node secondWord = searchWord(mRoots, word1, 0, null);
- LinkedList<NextWord> bigrams = firstWord.mNGrams;
- if (bigrams == null || bigrams.size() == 0) {
- firstWord.mNGrams = CollectionUtils.newLinkedList();
- bigrams = firstWord.mNGrams;
- } else {
- for (NextWord nw : bigrams) {
- if (nw.getWordNode() == secondWord) {
- return nw.notifyTypedAgainAndGetFrequency();
- }
- }
- }
- if (fcp != null) {
- // history
- firstWord.mNGrams.add(new NextHistoryWord(secondWord, fcp));
- } else {
- firstWord.mNGrams.add(new NextStaticWord(secondWord, frequency));
- }
- return frequency;
- }
-
- /**
- * Searches for the word and add the word if it does not exist.
- * @return Returns the terminal node of the word we are searching for.
- */
- private Node searchWord(final NodeArray children, final String word, final int depth,
- final Node parentNode) {
- final int wordLength = word.length();
- final char c = word.charAt(depth);
- // Does children have the current character?
- final int childrenLength = children.mLength;
- Node childNode = null;
- for (int i = 0; i < childrenLength; i++) {
- final Node node = children.mData[i];
- if (node.mCode == c) {
- childNode = node;
- break;
- }
- }
- if (childNode == null) {
- childNode = new Node();
- childNode.mCode = c;
- childNode.mParent = parentNode;
- children.add(childNode);
- }
- if (wordLength == depth + 1) {
- // Terminate this word
- childNode.mTerminal = true;
- return childNode;
- }
- if (childNode.mChildren == null) {
- childNode.mChildren = new NodeArray();
- }
- return searchWord(childNode.mChildren, word, depth + 1, childNode);
- }
-
- private void runBigramReverseLookUp(final String previousWord,
- final ArrayList<SuggestedWordInfo> suggestions) {
- // Search for the lowercase version of the word only, because that's where bigrams
- // store their sons.
- final Node prevWord = searchNode(mRoots, previousWord.toLowerCase(), 0,
- previousWord.length());
- if (prevWord != null && prevWord.mNGrams != null) {
- reverseLookUp(prevWord.mNGrams, suggestions);
- }
- }
-
- // Local to reverseLookUp, but do not allocate each time.
- private final char[] mLookedUpString = new char[Constants.DICTIONARY_MAX_WORD_LENGTH];
-
- /**
- * reverseLookUp retrieves the full word given a list of terminal nodes and adds those words
- * to the suggestions list passed as an argument.
- * @param terminalNodes list of terminal nodes we want to add
- * @param suggestions the suggestion collection to add the word to
- */
- private void reverseLookUp(final LinkedList<NextWord> terminalNodes,
- final ArrayList<SuggestedWordInfo> suggestions) {
- Node node;
- int freq;
- for (NextWord nextWord : terminalNodes) {
- node = nextWord.getWordNode();
- freq = nextWord.getFrequency();
- int index = Constants.DICTIONARY_MAX_WORD_LENGTH;
- do {
- --index;
- mLookedUpString[index] = node.mCode;
- node = node.mParent;
- } while (node != null && index > 0);
-
- // If node is null, we have a word longer than MAX_WORD_LENGTH in the dictionary.
- // It's a little unclear how this can happen, but just in case it does it's safer
- // to ignore the word in this case.
- if (freq >= 0 && node == null) {
- suggestions.add(new SuggestedWordInfo(new String(mLookedUpString, index,
- Constants.DICTIONARY_MAX_WORD_LENGTH - index),
- freq, SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */,
- SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
- SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
- }
- }
- }
-
- /**
- * Recursively search for the terminal node of the word.
- *
- * One iteration takes the full word to search for and the current index of the recursion.
- *
- * @param children the node of the trie to search under.
- * @param word the word to search for. Only read [offset..length] so there may be trailing chars
- * @param offset the index in {@code word} this recursion should operate on.
- * @param length the length of the input word.
- * @return Returns the terminal node of the word if the word exists
- */
- private Node searchNode(final NodeArray children, final CharSequence word, final int offset,
- final int length) {
- final int count = children.mLength;
- final char currentChar = word.charAt(offset);
- for (int j = 0; j < count; j++) {
- final Node node = children.mData[j];
- if (node.mCode == currentChar) {
- if (offset == length - 1) {
- if (node.mTerminal) {
- return node;
- }
- } else {
- if (node.mChildren != null) {
- Node returnNode = searchNode(node.mChildren, word, offset + 1, length);
- if (returnNode != null) return returnNode;
- }
- }
- }
- }
- return null;
- }
-
- public void clearDictionary() {
- mRoots = new NodeArray();
- }
-
- private static char toLowerCase(final char c) {
- char baseChar = c;
- if (c < BASE_CHARS.length) {
- baseChar = BASE_CHARS[c];
- }
- if (baseChar >= 'A' && baseChar <= 'Z') {
- return (char)(baseChar | 32);
- } else if (baseChar > 127) {
- return Character.toLowerCase(baseChar);
- }
- return baseChar;
- }
-
- /**
- * Table mapping most combined Latin, Greek, and Cyrillic characters
- * to their base characters. If c is in range, BASE_CHARS[c] == c
- * if c is not a combined character, or the base character if it
- * is combined.
- *
- * cf. native/jni/src/utils/char_utils.cpp
- */
- private static final char BASE_CHARS[] = {
- /* U+0000 */ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
- /* U+0008 */ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
- /* U+0010 */ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
- /* U+0018 */ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
- /* U+0020 */ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
- /* U+0028 */ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
- /* U+0030 */ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
- /* U+0038 */ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
- /* U+0040 */ 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
- /* U+0048 */ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
- /* U+0050 */ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
- /* U+0058 */ 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
- /* U+0060 */ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
- /* U+0068 */ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
- /* U+0070 */ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
- /* U+0078 */ 0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F,
- /* U+0080 */ 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087,
- /* U+0088 */ 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F,
- /* U+0090 */ 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097,
- /* U+0098 */ 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F,
- /* U+00A0 */ 0x0020, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7,
- /* U+00A8 */ 0x0020, 0x00A9, 0x0061, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0020,
- /* U+00B0 */ 0x00B0, 0x00B1, 0x0032, 0x0033, 0x0020, 0x03BC, 0x00B6, 0x00B7,
- /* U+00B8 */ 0x0020, 0x0031, 0x006F, 0x00BB, 0x0031, 0x0031, 0x0033, 0x00BF,
- /* U+00C0 */ 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00C6, 0x0043,
- /* U+00C8 */ 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049,
- /* U+00D0 */ 0x00D0, 0x004E, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x00D7,
- /* U+00D8 */ 0x004F, 0x0055, 0x0055, 0x0055, 0x0055, 0x0059, 0x00DE, 0x0073,
- // U+00D8: Manually changed from 00D8 to 004F
- // TODO: Check if it's really acceptable to consider Ø a diacritical variant of O
- // U+00DF: Manually changed from 00DF to 0073
- /* U+00E0 */ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00E6, 0x0063,
- /* U+00E8 */ 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069,
- /* U+00F0 */ 0x00F0, 0x006E, 0x006F, 0x006F, 0x006F, 0x006F, 0x006F, 0x00F7,
- /* U+00F8 */ 0x006F, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00FE, 0x0079,
- // U+00F8: Manually changed from 00F8 to 006F
- // TODO: Check if it's really acceptable to consider ø a diacritical variant of o
- /* U+0100 */ 0x0041, 0x0061, 0x0041, 0x0061, 0x0041, 0x0061, 0x0043, 0x0063,
- /* U+0108 */ 0x0043, 0x0063, 0x0043, 0x0063, 0x0043, 0x0063, 0x0044, 0x0064,
- /* U+0110 */ 0x0110, 0x0111, 0x0045, 0x0065, 0x0045, 0x0065, 0x0045, 0x0065,
- /* U+0118 */ 0x0045, 0x0065, 0x0045, 0x0065, 0x0047, 0x0067, 0x0047, 0x0067,
- /* U+0120 */ 0x0047, 0x0067, 0x0047, 0x0067, 0x0048, 0x0068, 0x0126, 0x0127,
- /* U+0128 */ 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069,
- /* U+0130 */ 0x0049, 0x0131, 0x0049, 0x0069, 0x004A, 0x006A, 0x004B, 0x006B,
- /* U+0138 */ 0x0138, 0x004C, 0x006C, 0x004C, 0x006C, 0x004C, 0x006C, 0x004C,
- /* U+0140 */ 0x006C, 0x004C, 0x006C, 0x004E, 0x006E, 0x004E, 0x006E, 0x004E,
- // U+0141: Manually changed from 0141 to 004C
- // U+0142: Manually changed from 0142 to 006C
- /* U+0148 */ 0x006E, 0x02BC, 0x014A, 0x014B, 0x004F, 0x006F, 0x004F, 0x006F,
- /* U+0150 */ 0x004F, 0x006F, 0x0152, 0x0153, 0x0052, 0x0072, 0x0052, 0x0072,
- /* U+0158 */ 0x0052, 0x0072, 0x0053, 0x0073, 0x0053, 0x0073, 0x0053, 0x0073,
- /* U+0160 */ 0x0053, 0x0073, 0x0054, 0x0074, 0x0054, 0x0074, 0x0166, 0x0167,
- /* U+0168 */ 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075,
- /* U+0170 */ 0x0055, 0x0075, 0x0055, 0x0075, 0x0057, 0x0077, 0x0059, 0x0079,
- /* U+0178 */ 0x0059, 0x005A, 0x007A, 0x005A, 0x007A, 0x005A, 0x007A, 0x0073,
- /* U+0180 */ 0x0180, 0x0181, 0x0182, 0x0183, 0x0184, 0x0185, 0x0186, 0x0187,
- /* U+0188 */ 0x0188, 0x0189, 0x018A, 0x018B, 0x018C, 0x018D, 0x018E, 0x018F,
- /* U+0190 */ 0x0190, 0x0191, 0x0192, 0x0193, 0x0194, 0x0195, 0x0196, 0x0197,
- /* U+0198 */ 0x0198, 0x0199, 0x019A, 0x019B, 0x019C, 0x019D, 0x019E, 0x019F,
- /* U+01A0 */ 0x004F, 0x006F, 0x01A2, 0x01A3, 0x01A4, 0x01A5, 0x01A6, 0x01A7,
- /* U+01A8 */ 0x01A8, 0x01A9, 0x01AA, 0x01AB, 0x01AC, 0x01AD, 0x01AE, 0x0055,
- /* U+01B0 */ 0x0075, 0x01B1, 0x01B2, 0x01B3, 0x01B4, 0x01B5, 0x01B6, 0x01B7,
- /* U+01B8 */ 0x01B8, 0x01B9, 0x01BA, 0x01BB, 0x01BC, 0x01BD, 0x01BE, 0x01BF,
- /* U+01C0 */ 0x01C0, 0x01C1, 0x01C2, 0x01C3, 0x0044, 0x0044, 0x0064, 0x004C,
- /* U+01C8 */ 0x004C, 0x006C, 0x004E, 0x004E, 0x006E, 0x0041, 0x0061, 0x0049,
- /* U+01D0 */ 0x0069, 0x004F, 0x006F, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055,
- // U+01D5: Manually changed from 00DC to 0055
- // U+01D6: Manually changed from 00FC to 0075
- // U+01D7: Manually changed from 00DC to 0055
- /* U+01D8 */ 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x01DD, 0x0041, 0x0061,
- // U+01D8: Manually changed from 00FC to 0075
- // U+01D9: Manually changed from 00DC to 0055
- // U+01DA: Manually changed from 00FC to 0075
- // U+01DB: Manually changed from 00DC to 0055
- // U+01DC: Manually changed from 00FC to 0075
- // U+01DE: Manually changed from 00C4 to 0041
- // U+01DF: Manually changed from 00E4 to 0061
- /* U+01E0 */ 0x0041, 0x0061, 0x00C6, 0x00E6, 0x01E4, 0x01E5, 0x0047, 0x0067,
- // U+01E0: Manually changed from 0226 to 0041
- // U+01E1: Manually changed from 0227 to 0061
- /* U+01E8 */ 0x004B, 0x006B, 0x004F, 0x006F, 0x004F, 0x006F, 0x01B7, 0x0292,
- // U+01EC: Manually changed from 01EA to 004F
- // U+01ED: Manually changed from 01EB to 006F
- /* U+01F0 */ 0x006A, 0x0044, 0x0044, 0x0064, 0x0047, 0x0067, 0x01F6, 0x01F7,
- /* U+01F8 */ 0x004E, 0x006E, 0x0041, 0x0061, 0x00C6, 0x00E6, 0x004F, 0x006F,
- // U+01FA: Manually changed from 00C5 to 0041
- // U+01FB: Manually changed from 00E5 to 0061
- // U+01FE: Manually changed from 00D8 to 004F
- // TODO: Check if it's really acceptable to consider Ø a diacritical variant of O
- // U+01FF: Manually changed from 00F8 to 006F
- // TODO: Check if it's really acceptable to consider ø a diacritical variant of o
- /* U+0200 */ 0x0041, 0x0061, 0x0041, 0x0061, 0x0045, 0x0065, 0x0045, 0x0065,
- /* U+0208 */ 0x0049, 0x0069, 0x0049, 0x0069, 0x004F, 0x006F, 0x004F, 0x006F,
- /* U+0210 */ 0x0052, 0x0072, 0x0052, 0x0072, 0x0055, 0x0075, 0x0055, 0x0075,
- /* U+0218 */ 0x0053, 0x0073, 0x0054, 0x0074, 0x021C, 0x021D, 0x0048, 0x0068,
- /* U+0220 */ 0x0220, 0x0221, 0x0222, 0x0223, 0x0224, 0x0225, 0x0041, 0x0061,
- /* U+0228 */ 0x0045, 0x0065, 0x004F, 0x006F, 0x004F, 0x006F, 0x004F, 0x006F,
- // U+022A: Manually changed from 00D6 to 004F
- // U+022B: Manually changed from 00F6 to 006F
- // U+022C: Manually changed from 00D5 to 004F
- // U+022D: Manually changed from 00F5 to 006F
- /* U+0230 */ 0x004F, 0x006F, 0x0059, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237,
- // U+0230: Manually changed from 022E to 004F
- // U+0231: Manually changed from 022F to 006F
- /* U+0238 */ 0x0238, 0x0239, 0x023A, 0x023B, 0x023C, 0x023D, 0x023E, 0x023F,
- /* U+0240 */ 0x0240, 0x0241, 0x0242, 0x0243, 0x0244, 0x0245, 0x0246, 0x0247,
- /* U+0248 */ 0x0248, 0x0249, 0x024A, 0x024B, 0x024C, 0x024D, 0x024E, 0x024F,
- /* U+0250 */ 0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257,
- /* U+0258 */ 0x0258, 0x0259, 0x025A, 0x025B, 0x025C, 0x025D, 0x025E, 0x025F,
- /* U+0260 */ 0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267,
- /* U+0268 */ 0x0268, 0x0269, 0x026A, 0x026B, 0x026C, 0x026D, 0x026E, 0x026F,
- /* U+0270 */ 0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277,
- /* U+0278 */ 0x0278, 0x0279, 0x027A, 0x027B, 0x027C, 0x027D, 0x027E, 0x027F,
- /* U+0280 */ 0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287,
- /* U+0288 */ 0x0288, 0x0289, 0x028A, 0x028B, 0x028C, 0x028D, 0x028E, 0x028F,
- /* U+0290 */ 0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297,
- /* U+0298 */ 0x0298, 0x0299, 0x029A, 0x029B, 0x029C, 0x029D, 0x029E, 0x029F,
- /* U+02A0 */ 0x02A0, 0x02A1, 0x02A2, 0x02A3, 0x02A4, 0x02A5, 0x02A6, 0x02A7,
- /* U+02A8 */ 0x02A8, 0x02A9, 0x02AA, 0x02AB, 0x02AC, 0x02AD, 0x02AE, 0x02AF,
- /* U+02B0 */ 0x0068, 0x0266, 0x006A, 0x0072, 0x0279, 0x027B, 0x0281, 0x0077,
- /* U+02B8 */ 0x0079, 0x02B9, 0x02BA, 0x02BB, 0x02BC, 0x02BD, 0x02BE, 0x02BF,
- /* U+02C0 */ 0x02C0, 0x02C1, 0x02C2, 0x02C3, 0x02C4, 0x02C5, 0x02C6, 0x02C7,
- /* U+02C8 */ 0x02C8, 0x02C9, 0x02CA, 0x02CB, 0x02CC, 0x02CD, 0x02CE, 0x02CF,
- /* U+02D0 */ 0x02D0, 0x02D1, 0x02D2, 0x02D3, 0x02D4, 0x02D5, 0x02D6, 0x02D7,
- /* U+02D8 */ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02DE, 0x02DF,
- /* U+02E0 */ 0x0263, 0x006C, 0x0073, 0x0078, 0x0295, 0x02E5, 0x02E6, 0x02E7,
- /* U+02E8 */ 0x02E8, 0x02E9, 0x02EA, 0x02EB, 0x02EC, 0x02ED, 0x02EE, 0x02EF,
- /* U+02F0 */ 0x02F0, 0x02F1, 0x02F2, 0x02F3, 0x02F4, 0x02F5, 0x02F6, 0x02F7,
- /* U+02F8 */ 0x02F8, 0x02F9, 0x02FA, 0x02FB, 0x02FC, 0x02FD, 0x02FE, 0x02FF,
- /* U+0300 */ 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307,
- /* U+0308 */ 0x0308, 0x0309, 0x030A, 0x030B, 0x030C, 0x030D, 0x030E, 0x030F,
- /* U+0310 */ 0x0310, 0x0311, 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317,
- /* U+0318 */ 0x0318, 0x0319, 0x031A, 0x031B, 0x031C, 0x031D, 0x031E, 0x031F,
- /* U+0320 */ 0x0320, 0x0321, 0x0322, 0x0323, 0x0324, 0x0325, 0x0326, 0x0327,
- /* U+0328 */ 0x0328, 0x0329, 0x032A, 0x032B, 0x032C, 0x032D, 0x032E, 0x032F,
- /* U+0330 */ 0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, 0x0336, 0x0337,
- /* U+0338 */ 0x0338, 0x0339, 0x033A, 0x033B, 0x033C, 0x033D, 0x033E, 0x033F,
- /* U+0340 */ 0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x0345, 0x0346, 0x0347,
- /* U+0348 */ 0x0348, 0x0349, 0x034A, 0x034B, 0x034C, 0x034D, 0x034E, 0x034F,
- /* U+0350 */ 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357,
- /* U+0358 */ 0x0358, 0x0359, 0x035A, 0x035B, 0x035C, 0x035D, 0x035E, 0x035F,
- /* U+0360 */ 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367,
- /* U+0368 */ 0x0368, 0x0369, 0x036A, 0x036B, 0x036C, 0x036D, 0x036E, 0x036F,
- /* U+0370 */ 0x0370, 0x0371, 0x0372, 0x0373, 0x02B9, 0x0375, 0x0376, 0x0377,
- /* U+0378 */ 0x0378, 0x0379, 0x0020, 0x037B, 0x037C, 0x037D, 0x003B, 0x037F,
- /* U+0380 */ 0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x00A8, 0x0391, 0x00B7,
- /* U+0388 */ 0x0395, 0x0397, 0x0399, 0x038B, 0x039F, 0x038D, 0x03A5, 0x03A9,
- /* U+0390 */ 0x03CA, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397,
- /* U+0398 */ 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F,
- /* U+03A0 */ 0x03A0, 0x03A1, 0x03A2, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7,
- /* U+03A8 */ 0x03A8, 0x03A9, 0x0399, 0x03A5, 0x03B1, 0x03B5, 0x03B7, 0x03B9,
- /* U+03B0 */ 0x03CB, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7,
- /* U+03B8 */ 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF,
- /* U+03C0 */ 0x03C0, 0x03C1, 0x03C2, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7,
- /* U+03C8 */ 0x03C8, 0x03C9, 0x03B9, 0x03C5, 0x03BF, 0x03C5, 0x03C9, 0x03CF,
- /* U+03D0 */ 0x03B2, 0x03B8, 0x03A5, 0x03D2, 0x03D2, 0x03C6, 0x03C0, 0x03D7,
- /* U+03D8 */ 0x03D8, 0x03D9, 0x03DA, 0x03DB, 0x03DC, 0x03DD, 0x03DE, 0x03DF,
- /* U+03E0 */ 0x03E0, 0x03E1, 0x03E2, 0x03E3, 0x03E4, 0x03E5, 0x03E6, 0x03E7,
- /* U+03E8 */ 0x03E8, 0x03E9, 0x03EA, 0x03EB, 0x03EC, 0x03ED, 0x03EE, 0x03EF,
- /* U+03F0 */ 0x03BA, 0x03C1, 0x03C2, 0x03F3, 0x0398, 0x03B5, 0x03F6, 0x03F7,
- /* U+03F8 */ 0x03F8, 0x03A3, 0x03FA, 0x03FB, 0x03FC, 0x03FD, 0x03FE, 0x03FF,
- /* U+0400 */ 0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406,
- /* U+0408 */ 0x0408, 0x0409, 0x040A, 0x040B, 0x041A, 0x0418, 0x0423, 0x040F,
- /* U+0410 */ 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
- /* U+0418 */ 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F,
- // U+0419: Manually changed from 0418 to 0419
- /* U+0420 */ 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
- /* U+0428 */ 0x0428, 0x0429, 0x042C, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F,
- // U+042A: Manually changed from 042A to 042C
- /* U+0430 */ 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
- /* U+0438 */ 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F,
- // U+0439: Manually changed from 0438 to 0439
- /* U+0440 */ 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
- /* U+0448 */ 0x0448, 0x0449, 0x044C, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F,
- // U+044A: Manually changed from 044A to 044C
- /* U+0450 */ 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456,
- /* U+0458 */ 0x0458, 0x0459, 0x045A, 0x045B, 0x043A, 0x0438, 0x0443, 0x045F,
- /* U+0460 */ 0x0460, 0x0461, 0x0462, 0x0463, 0x0464, 0x0465, 0x0466, 0x0467,
- /* U+0468 */ 0x0468, 0x0469, 0x046A, 0x046B, 0x046C, 0x046D, 0x046E, 0x046F,
- /* U+0470 */ 0x0470, 0x0471, 0x0472, 0x0473, 0x0474, 0x0475, 0x0474, 0x0475,
- /* U+0478 */ 0x0478, 0x0479, 0x047A, 0x047B, 0x047C, 0x047D, 0x047E, 0x047F,
- /* U+0480 */ 0x0480, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487,
- /* U+0488 */ 0x0488, 0x0489, 0x048A, 0x048B, 0x048C, 0x048D, 0x048E, 0x048F,
- /* U+0490 */ 0x0490, 0x0491, 0x0492, 0x0493, 0x0494, 0x0495, 0x0496, 0x0497,
- /* U+0498 */ 0x0498, 0x0499, 0x049A, 0x049B, 0x049C, 0x049D, 0x049E, 0x049F,
- /* U+04A0 */ 0x04A0, 0x04A1, 0x04A2, 0x04A3, 0x04A4, 0x04A5, 0x04A6, 0x04A7,
- /* U+04A8 */ 0x04A8, 0x04A9, 0x04AA, 0x04AB, 0x04AC, 0x04AD, 0x04AE, 0x04AF,
- /* U+04B0 */ 0x04B0, 0x04B1, 0x04B2, 0x04B3, 0x04B4, 0x04B5, 0x04B6, 0x04B7,
- /* U+04B8 */ 0x04B8, 0x04B9, 0x04BA, 0x04BB, 0x04BC, 0x04BD, 0x04BE, 0x04BF,
- /* U+04C0 */ 0x04C0, 0x0416, 0x0436, 0x04C3, 0x04C4, 0x04C5, 0x04C6, 0x04C7,
- /* U+04C8 */ 0x04C8, 0x04C9, 0x04CA, 0x04CB, 0x04CC, 0x04CD, 0x04CE, 0x04CF,
- /* U+04D0 */ 0x0410, 0x0430, 0x0410, 0x0430, 0x04D4, 0x04D5, 0x0415, 0x0435,
- /* U+04D8 */ 0x04D8, 0x04D9, 0x04D8, 0x04D9, 0x0416, 0x0436, 0x0417, 0x0437,
- /* U+04E0 */ 0x04E0, 0x04E1, 0x0418, 0x0438, 0x0418, 0x0438, 0x041E, 0x043E,
- /* U+04E8 */ 0x04E8, 0x04E9, 0x04E8, 0x04E9, 0x042D, 0x044D, 0x0423, 0x0443,
- /* U+04F0 */ 0x0423, 0x0443, 0x0423, 0x0443, 0x0427, 0x0447, 0x04F6, 0x04F7,
- /* U+04F8 */ 0x042B, 0x044B, 0x04FA, 0x04FB, 0x04FC, 0x04FD, 0x04FE, 0x04FF,
- };
-}
diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java
index 8caf6f17f..b01bc4ba5 100644
--- a/java/src/com/android/inputmethod/latin/InputAttributes.java
+++ b/java/src/com/android/inputmethod/latin/InputAttributes.java
@@ -29,6 +29,7 @@ import com.android.inputmethod.latin.utils.StringUtils;
public final class InputAttributes {
private final String TAG = InputAttributes.class.getSimpleName();
+ final public String mTargetApplicationPackageName;
final public boolean mInputTypeNoAutoCorrect;
final public boolean mIsSettingsSuggestionStripOn;
final public boolean mApplicationSpecifiedCompletionOn;
@@ -36,6 +37,7 @@ public final class InputAttributes {
final private int mInputType;
public InputAttributes(final EditorInfo editorInfo, final boolean isFullscreenMode) {
+ mTargetApplicationPackageName = null != editorInfo ? editorInfo.packageName : null;
final int inputType = null != editorInfo ? editorInfo.inputType : 0;
final int inputClass = inputType & InputType.TYPE_MASK_CLASS;
mInputType = inputType;
@@ -52,8 +54,7 @@ public final class InputAttributes {
} else if (inputClass == 0) {
// TODO: is this check still necessary?
Log.w(TAG, String.format("Unexpected input class: inputType=0x%08x"
- + " imeOptions=0x%08x",
- inputType, editorInfo.imeOptions));
+ + " imeOptions=0x%08x", inputType, editorInfo.imeOptions));
}
mIsSettingsSuggestionStripOn = false;
mInputTypeNoAutoCorrect = false;
@@ -204,8 +205,7 @@ public final class InputAttributes {
public static boolean inPrivateImeOptions(String packageName, String key,
EditorInfo editorInfo) {
if (editorInfo == null) return false;
- final String findingKey = (packageName != null) ? packageName + "." + key
- : key;
+ final String findingKey = (packageName != null) ? packageName + "." + key : key;
return StringUtils.containsInCommaSplittableText(findingKey, editorInfo.privateImeOptions);
}
}
diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java
index 81ccf83d8..76b0912f6 100644
--- a/java/src/com/android/inputmethod/latin/InputView.java
+++ b/java/src/com/android/inputmethod/latin/InputView.java
@@ -23,87 +23,203 @@ import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
-public final class InputView extends LinearLayout {
- private View mSuggestionStripView;
- private View mKeyboardView;
- private int mKeyboardTopPadding;
+import com.android.inputmethod.keyboard.MainKeyboardView;
+import com.android.inputmethod.latin.suggestions.MoreSuggestionsView;
+import com.android.inputmethod.latin.suggestions.SuggestionStripView;
- private boolean mIsForwardingEvent;
+public final class InputView extends LinearLayout {
private final Rect mInputViewRect = new Rect();
- private final Rect mEventForwardingRect = new Rect();
- private final Rect mEventReceivingRect = new Rect();
+ private KeyboardTopPaddingForwarder mKeyboardTopPaddingForwarder;
+ private MoreSuggestionsViewCanceler mMoreSuggestionsViewCanceler;
public InputView(final Context context, final AttributeSet attrs) {
super(context, attrs, 0);
}
- public void setKeyboardGeometry(final int keyboardTopPadding) {
- mKeyboardTopPadding = keyboardTopPadding;
- }
-
@Override
protected void onFinishInflate() {
- mSuggestionStripView = findViewById(R.id.suggestion_strip_view);
- mKeyboardView = findViewById(R.id.keyboard_view);
+ final SuggestionStripView suggestionStripView =
+ (SuggestionStripView)findViewById(R.id.suggestion_strip_view);
+ final MainKeyboardView mainKeyboardView =
+ (MainKeyboardView)findViewById(R.id.keyboard_view);
+ mKeyboardTopPaddingForwarder = new KeyboardTopPaddingForwarder(
+ mainKeyboardView, suggestionStripView);
+ mMoreSuggestionsViewCanceler = new MoreSuggestionsViewCanceler(
+ mainKeyboardView, suggestionStripView);
+ }
+
+ public void setKeyboardTopPadding(final int keyboardTopPadding) {
+ mKeyboardTopPaddingForwarder.setKeyboardTopPadding(keyboardTopPadding);
}
@Override
public boolean dispatchTouchEvent(final MotionEvent me) {
- if (mSuggestionStripView.getVisibility() != VISIBLE
- || mKeyboardView.getVisibility() != VISIBLE) {
- return super.dispatchTouchEvent(me);
- }
-
- // The touch events that hit the top padding of keyboard should be forwarded to
- // {@link SuggestionStripView}.
final Rect rect = mInputViewRect;
- this.getGlobalVisibleRect(rect);
+ getGlobalVisibleRect(rect);
final int x = (int)me.getX() + rect.left;
final int y = (int)me.getY() + rect.top;
- final Rect forwardingRect = mEventForwardingRect;
- mKeyboardView.getGlobalVisibleRect(forwardingRect);
- if (!mIsForwardingEvent && !forwardingRect.contains(x, y)) {
- return super.dispatchTouchEvent(me);
+ // The touch events that hit the top padding of keyboard should be
+ // forwarded to {@link SuggestionStripView}.
+ if (mKeyboardTopPaddingForwarder.dispatchTouchEvent(x, y, me)) {
+ return true;
+ }
+ // To cancel {@link MoreSuggestionsView}, we should intercept a touch event to
+ // {@link MainKeyboardView} and dismiss the {@link MoreSuggestionsView}.
+ if (mMoreSuggestionsViewCanceler.dispatchTouchEvent(x, y, me)) {
+ return true;
+ }
+ return super.dispatchTouchEvent(me);
+ }
+
+ /**
+ * This class forwards series of {@link MotionEvent}s from <code>Forwarder</code> view to
+ * <code>Receiver</code> view.
+ *
+ * @param <Sender> a {@link View} that may send a {@link MotionEvent} to <Receiver>.
+ * @param <Receiver> a {@link View} that receives forwarded {@link MotionEvent} from
+ * <Forwarder>.
+ */
+ private static abstract class MotionEventForwarder<Sender extends View, Receiver extends View> {
+ protected final Sender mSenderView;
+ protected final Receiver mReceiverView;
+
+ private boolean mIsForwardingEvent;
+ protected final Rect mEventSendingRect = new Rect();
+ protected final Rect mEventReceivingRect = new Rect();
+
+ public MotionEventForwarder(final Sender senderView, final Receiver receiverView) {
+ mSenderView = senderView;
+ mReceiverView = receiverView;
}
- final int forwardingLimitY = forwardingRect.top + mKeyboardTopPadding;
- boolean sendToTarget = false;
+ // Return true if a touch event of global coordinate x, y needs to be forwarded.
+ protected abstract boolean needsToForward(final int x, final int y);
- switch (me.getAction()) {
- case MotionEvent.ACTION_DOWN:
- if (y < forwardingLimitY) {
- // This down event and further move and up events should be forwarded to the target.
- mIsForwardingEvent = true;
- sendToTarget = true;
+ // Translate global x-coordinate to <code>Receiver</code> local coordinate.
+ protected int translateX(final int x) {
+ return x - mEventReceivingRect.left;
+ }
+
+ // Translate global y-coordinate to <code>Receiver</code> local coordinate.
+ protected int translateY(final int y) {
+ return y - mEventReceivingRect.top;
+ }
+
+ // Callback when a {@link MotionEvent} is forwarded.
+ protected void onForwardingEvent(final MotionEvent me) {}
+
+ // Dispatches a {@link MotioneEvent} to <code>Receiver</code> if needed and returns true.
+ // Otherwise returns false.
+ public boolean dispatchTouchEvent(final int x, final int y, final MotionEvent me) {
+ // Forwards a {link MotionEvent} only if both <code>Sender</code> and
+ // <code>Receiver</code> are visible.
+ if (mSenderView.getVisibility() != View.VISIBLE ||
+ mReceiverView.getVisibility() != View.VISIBLE) {
+ return false;
+ }
+ final Rect sendingRect = mEventSendingRect;
+ mSenderView.getGlobalVisibleRect(sendingRect);
+ if (!mIsForwardingEvent && !sendingRect.contains(x, y)) {
+ return false;
}
- break;
- case MotionEvent.ACTION_MOVE:
- sendToTarget = mIsForwardingEvent;
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- sendToTarget = mIsForwardingEvent;
- mIsForwardingEvent = false;
- break;
+
+ boolean shouldForwardToReceiver = false;
+
+ switch (me.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ // If the down event happens in the forwarding area, successive {@link MotionEvent}s
+ // should be forwarded.
+ if (needsToForward(x, y)) {
+ mIsForwardingEvent = true;
+ shouldForwardToReceiver = true;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ shouldForwardToReceiver = mIsForwardingEvent;
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ shouldForwardToReceiver = mIsForwardingEvent;
+ mIsForwardingEvent = false;
+ break;
+ }
+
+ if (!shouldForwardToReceiver) {
+ return false;
+ }
+
+ final Rect receivingRect = mEventReceivingRect;
+ mReceiverView.getGlobalVisibleRect(receivingRect);
+ // Translate global coordinates to <code>Receiver</code> local coordinates.
+ me.setLocation(translateX(x), translateY(y));
+ mReceiverView.dispatchTouchEvent(me);
+ onForwardingEvent(me);
+ return true;
}
+ }
+
+ /**
+ * This class forwards {@link MotionEvent}s happened in the top padding of
+ * {@link MainKeyboardView} to {@link SuggestionStripView}.
+ */
+ private static class KeyboardTopPaddingForwarder
+ extends MotionEventForwarder<MainKeyboardView, SuggestionStripView> {
+ private int mKeyboardTopPadding;
- if (!sendToTarget) {
- return super.dispatchTouchEvent(me);
+ public KeyboardTopPaddingForwarder(final MainKeyboardView mainKeyboardView,
+ final SuggestionStripView suggestionStripView) {
+ super(mainKeyboardView, suggestionStripView);
}
- final Rect receivingRect = mEventReceivingRect;
- mSuggestionStripView.getGlobalVisibleRect(receivingRect);
- final int translatedX = x - receivingRect.left;
- final int translatedY;
- if (y < forwardingLimitY) {
- // The forwarded event should have coordinates that are inside of the target.
- translatedY = Math.min(y - receivingRect.top, receivingRect.height() - 1);
- } else {
- translatedY = y - receivingRect.top;
+ public void setKeyboardTopPadding(final int keyboardTopPadding) {
+ mKeyboardTopPadding = keyboardTopPadding;
+ }
+
+ private boolean isInKeyboardTopPadding(final int y) {
+ return y < mEventSendingRect.top + mKeyboardTopPadding;
+ }
+
+ @Override
+ protected boolean needsToForward(final int x, final int y) {
+ return isInKeyboardTopPadding(y);
+ }
+
+ @Override
+ protected int translateY(final int y) {
+ final int translatedY = super.translateY(y);
+ if (isInKeyboardTopPadding(y)) {
+ // The forwarded event should have coordinates that are inside of
+ // the target.
+ return Math.min(translatedY, mEventReceivingRect.height() - 1);
+ }
+ return translatedY;
+ }
+ }
+
+ /**
+ * This class forwards {@link MotionEvent}s happened in the {@link MainKeyboardView} to
+ * {@link SuggestionStripView} when the {@link MoreSuggestionsView} is showing.
+ * {@link SuggestionStripView} dismisses {@link MoreSuggestionsView} when it receives those
+ * events.
+ */
+ private static class MoreSuggestionsViewCanceler
+ extends MotionEventForwarder<MainKeyboardView, SuggestionStripView> {
+ public MoreSuggestionsViewCanceler(final MainKeyboardView mainKeyboardView,
+ final SuggestionStripView suggestionStripView) {
+ super(mainKeyboardView, suggestionStripView);
+ }
+
+ @Override
+ protected boolean needsToForward(final int x, final int y) {
+ return mReceiverView.isShowingMoreSuggestionPanel() && mEventSendingRect.contains(x, y);
+ }
+
+ @Override
+ protected void onForwardingEvent(final MotionEvent me) {
+ if (me.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mReceiverView.dismissMoreSuggestionsPanel();
+ }
}
- me.setLocation(translatedX, translatedY);
- mSuggestionStripView.dispatchTouchEvent(me);
- return true;
}
}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 77d07019f..8ea868d51 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -28,7 +28,6 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
-import android.content.pm.PackageInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
@@ -36,39 +35,30 @@ import android.inputmethodservice.InputMethodService;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.os.Debug;
-import android.os.Handler;
-import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.text.InputType;
import android.text.TextUtils;
-import android.text.style.SuggestionSpan;
import android.util.Log;
import android.util.Pair;
import android.util.PrintWriterPrinter;
import android.util.Printer;
-import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.CompletionInfo;
-import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.accessibility.AccessibilityUtils;
import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.compat.AppWorkaroundsUtils;
import com.android.inputmethod.compat.InputMethodServiceCompatUtils;
-import com.android.inputmethod.compat.SuggestionSpanUtils;
import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
-import com.android.inputmethod.event.EventInterpreter;
-import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardActionListener;
import com.android.inputmethod.keyboard.KeyboardId;
@@ -77,157 +67,76 @@ import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.latin.inputlogic.InputLogic;
+import com.android.inputmethod.latin.inputlogic.SpaceState;
import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever;
-import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
-import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister;
+import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegistrar;
import com.android.inputmethod.latin.personalization.PersonalizationHelper;
-import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary;
-import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
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.utils.ApplicationUtils;
-import com.android.inputmethod.latin.utils.AsyncResultHolder;
-import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
import com.android.inputmethod.latin.utils.CapsModeUtils;
-import com.android.inputmethod.latin.utils.CollectionUtils;
import com.android.inputmethod.latin.utils.CompletionInfoUtils;
-import com.android.inputmethod.latin.utils.InputTypeUtils;
import com.android.inputmethod.latin.utils.IntentUtils;
import com.android.inputmethod.latin.utils.JniUtils;
import com.android.inputmethod.latin.utils.LatinImeLoggerUtils;
-import com.android.inputmethod.latin.utils.RecapitalizeStatus;
-import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
-import com.android.inputmethod.latin.utils.StringUtils;
-import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
-import com.android.inputmethod.latin.utils.TextRange;
-import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils;
+import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper;
import com.android.inputmethod.research.ResearchLogger;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Locale;
-import java.util.TreeSet;
/**
* Input method implementation for Qwerty'ish keyboard.
*/
public class LatinIME extends InputMethodService implements KeyboardActionListener,
- SuggestionStripView.Listener, TargetPackageInfoGetterTask.OnTargetPackageInfoKnownListener,
- Suggest.SuggestInitializationListener {
+ SuggestionStripView.Listener,
+ DictionaryFacilitatorForSuggest.DictionaryInitializationListener {
private static final String TAG = LatinIME.class.getSimpleName();
private static final boolean TRACE = false;
- private static boolean DEBUG;
+ private static boolean DEBUG = false;
private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
- // How many continuous deletes at which to start deleting at a higher speed.
- private static final int DELETE_ACCELERATE_AT = 20;
- // Key events coming any faster than this are long-presses.
- private static final int QUICK_PRESS = 200;
-
private static final int PENDING_IMS_CALLBACK_DURATION = 800;
private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2;
- // TODO: Set this value appropriately.
- private static final int GET_SUGGESTED_WORDS_TIMEOUT = 200;
-
/**
* The name of the scheme used by the Package Manager to warn of a new package installation,
* replacement or removal.
*/
private static final String SCHEME_PACKAGE = "package";
- private static final int SPACE_STATE_NONE = 0;
- // Double space: the state where the user pressed space twice quickly, which LatinIME
- // resolved as period-space. Undoing this converts the period to a space.
- private static final int SPACE_STATE_DOUBLE = 1;
- // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip
- // have just been swapped. Undoing this swaps them back; the space is still considered weak.
- private static final int SPACE_STATE_SWAP_PUNCTUATION = 2;
- // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak
- // spaces happen when the user presses space, accepting the current suggestion (whether
- // it's an auto-correction or not).
- private static final int SPACE_STATE_WEAK = 3;
- // Phantom space: a not-yet-inserted space that should get inserted on the next input,
- // character provided it's not a separator. If it's a separator, the phantom space is dropped.
- // Phantom spaces happen when a user chooses a word from the suggestion strip.
- private static final int SPACE_STATE_PHANTOM = 4;
-
- // Current space state of the input method. This can be any of the above constants.
- private int mSpaceState;
-
private final Settings mSettings;
+ private final InputLogic mInputLogic = new InputLogic(this);
private View mExtractArea;
private View mKeyPreviewBackingView;
private SuggestionStripView mSuggestionStripView;
- // Never null
- private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
- private Suggest mSuggest;
+
private CompletionInfo[] mApplicationSpecifiedCompletions;
- private AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils();
private RichInputMethodManager mRichImm;
@UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
private final SubtypeSwitcher mSubtypeSwitcher;
private final SubtypeState mSubtypeState = new SubtypeState();
- // At start, create a default event interpreter that does nothing by passing it no decoder spec.
- // The event interpreter should never be null.
- private EventInterpreter mEventInterpreter = new EventInterpreter(this);
-
- private boolean mIsMainDictionaryAvailable;
- private UserBinaryDictionary mUserDictionary;
- private UserHistoryDictionary mUserHistoryDictionary;
- private PersonalizationPredictionDictionary mPersonalizationPredictionDictionary;
- private PersonalizationDictionary mPersonalizationDictionary;
- private boolean mIsUserDictionaryAvailable;
-
- private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
- private final WordComposer mWordComposer = new WordComposer();
- private final RichInputConnection mConnection = new RichInputConnection(this);
- private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
-
- // Keep track of the last selection range to decide if we need to show word alternatives
- private static final int NOT_A_CURSOR_POSITION = -1;
- private int mLastSelectionStart = NOT_A_CURSOR_POSITION;
- private int mLastSelectionEnd = NOT_A_CURSOR_POSITION;
-
- // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't
- // "expect" it, it means the user actually moved the cursor.
- private boolean mExpectingUpdateSelection;
- private int mDeleteCount;
- private long mLastKeyTime;
- private final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
- // Personalization debugging params
- private boolean mUseOnlyPersonalizationDictionaryForDebug = false;
- private boolean mBoostPersonalizationDictionaryForDebug = false;
-
- // Member variables for remembering the current device orientation.
- private int mDisplayOrientation;
// Object for reacting to adding/removing a dictionary pack.
private BroadcastReceiver mDictionaryPackInstallReceiver =
new DictionaryPackInstallBroadcastReceiver(this);
- // Keeps track of most recently inserted text (multi-character key) for reverting
- private String mEnteredText;
-
- // TODO: This boolean is persistent state and causes large side effects at unexpected times.
- // Find a way to remove it for readability.
- private boolean mIsAutoCorrectionIndicatorOn;
-
private AlertDialog mOptionsDialog;
private final boolean mIsHardwareAcceleratedDrawingEnabled;
public final UIHandler mHandler = new UIHandler(this);
- private InputUpdater mInputUpdater;
- public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
+ public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> {
private static final int MSG_UPDATE_SHIFT_STATE = 0;
private static final int MSG_PENDING_IMS_CALLBACK = 1;
private static final int MSG_UPDATE_SUGGESTION_STRIP = 2;
@@ -236,6 +145,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private static final int MSG_REOPEN_DICTIONARIES = 5;
private static final int MSG_ON_END_BATCH_INPUT = 6;
private static final int MSG_RESET_CACHES = 7;
+ // Update this when adding new messages
+ private static final int MSG_LAST = MSG_RESET_CACHES;
private static final int ARG1_NOT_GESTURE_INPUT = 0;
private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
@@ -248,12 +159,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private long mDoubleSpacePeriodTimeout;
private long mDoubleSpacePeriodTimerStart;
- public UIHandler(final LatinIME outerInstance) {
- super(outerInstance);
+ public UIHandler(final LatinIME ownerInstance) {
+ super(ownerInstance);
}
public void onCreate() {
- final Resources res = getOuterInstance().getResources();
+ final Resources res = getOwnerInstance().getResources();
mDelayUpdateSuggestions =
res.getInteger(R.integer.config_delay_update_suggestions);
mDelayUpdateShiftState =
@@ -264,11 +175,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void handleMessage(final Message msg) {
- final LatinIME latinIme = getOuterInstance();
+ final LatinIME latinIme = getOwnerInstance();
final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
switch (msg.what) {
case MSG_UPDATE_SUGGESTION_STRIP:
- latinIme.updateSuggestionStrip();
+ latinIme.mInputLogic.performUpdateSuggestionStripSync(
+ latinIme.mSettings.getCurrent(), this /* handler */);
break;
case MSG_UPDATE_SHIFT_STATE:
switcher.updateShiftState();
@@ -288,7 +200,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
break;
case MSG_RESUME_SUGGESTIONS:
- latinIme.restartSuggestionsOnWordTouchedByCursor();
+ latinIme.mInputLogic.restartSuggestionsOnWordTouchedByCursor(
+ latinIme.mSettings.getCurrent(), 0 /* offset */,
+ false /* includeResumedWordInSuggestions */, latinIme.mKeyboardSwitcher);
break;
case MSG_REOPEN_DICTIONARIES:
latinIme.initSuggest();
@@ -298,11 +212,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
postUpdateSuggestionStrip();
break;
case MSG_ON_END_BATCH_INPUT:
- latinIme.onEndBatchInputAsyncInternal((SuggestedWords) msg.obj);
+ latinIme.mInputLogic.endBatchInputAsyncInternal(latinIme.mSettings.getCurrent(),
+ (SuggestedWords) msg.obj, latinIme.mKeyboardSwitcher);
break;
case MSG_RESET_CACHES:
- latinIme.retryResetCaches(msg.arg1 == 1 /* tryResumeSuggestions */,
- msg.arg2 /* remainingTries */);
+ latinIme.mInputLogic.retryResetCaches(latinIme.mSettings.getCurrent(),
+ msg.arg1 == 1 /* tryResumeSuggestions */,
+ msg.arg2 /* remainingTries */,
+ latinIme.mKeyboardSwitcher, this);
break;
}
}
@@ -347,6 +264,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
removeMessages(MSG_UPDATE_SHIFT_STATE);
}
+ @UsedForTesting
+ public void removeAllMessages() {
+ for (int i = 0; i <= MSG_LAST; ++i) {
+ removeMessages(i);
+ }
+ }
+
public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
final boolean dismissGestureFloatingPreviewText) {
removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
@@ -401,7 +325,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
removeMessages(MSG_PENDING_IMS_CALLBACK);
resetPendingImsCallback();
mIsOrientationChanging = true;
- final LatinIME latinIme = getOuterInstance();
+ final LatinIME latinIme = getOwnerInstance();
if (latinIme.isInputViewShown()) {
latinIme.mKeyboardSwitcher.saveKeyboardState();
}
@@ -434,7 +358,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mIsOrientationChanging = false;
mPendingSuccessiveImsCallback = true;
}
- final LatinIME latinIme = getOuterInstance();
+ final LatinIME latinIme = getOwnerInstance();
executePendingImsCallback(latinIme, editorInfo, restarting);
latinIme.onStartInputInternal(editorInfo, restarting);
}
@@ -453,7 +377,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
PENDING_IMS_CALLBACK_DURATION);
}
- final LatinIME latinIme = getOuterInstance();
+ final LatinIME latinIme = getOwnerInstance();
executePendingImsCallback(latinIme, editorInfo, restarting);
latinIme.onStartInputViewInternal(editorInfo, restarting);
mAppliedEditorInfo = editorInfo;
@@ -465,7 +389,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Typically this is the first onFinishInputView after orientation changed.
mHasPendingFinishInputView = true;
} else {
- final LatinIME latinIme = getOuterInstance();
+ final LatinIME latinIme = getOwnerInstance();
latinIme.onFinishInputViewInternal(finishingInput);
mAppliedEditorInfo = null;
}
@@ -476,7 +400,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Typically this is the first onFinishInput after orientation changed.
mHasPendingFinishInput = true;
} else {
- final LatinIME latinIme = getOuterInstance();
+ final LatinIME latinIme = getOwnerInstance();
executePendingImsCallback(latinIme, null, false);
latinIme.onFinishInputInternal();
}
@@ -536,7 +460,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
KeyboardSwitcher.init(this);
AudioAndHapticFeedbackManager.init(this);
AccessibilityUtils.init(this);
- PersonalizationDictionarySessionRegister.init(this);
super.onCreate();
@@ -548,9 +471,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
initSuggest();
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest);
+ ResearchLogger.getInstance().init(this, mKeyboardSwitcher);
}
- mDisplayOrientation = getResources().getConfiguration().orientation;
// Register to receive ringer mode change and network state change.
// Also receive installation and removal of a dictionary pack.
@@ -570,17 +492,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this);
-
- mInputUpdater = new InputUpdater(this);
}
// Has to be package-visible for unit tests
@UsedForTesting
void loadSettings() {
final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
- final InputAttributes inputAttributes =
- new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
- mSettings.loadSettings(locale, inputAttributes);
+ final EditorInfo editorInfo = getCurrentInputEditorInfo();
+ final InputAttributes inputAttributes = new InputAttributes(editorInfo, isFullscreenMode());
+ mSettings.loadSettings(this, locale, inputAttributes);
AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent());
// To load the keyboard we need to load all the settings once, but resetting the
// contacts dictionary should be deferred until after the new layout has been displayed
@@ -589,16 +509,34 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// the layout; at this time, we need to skip resetting the contacts dictionary. It will
// be done later inside {@see #initSuggest()} when the reopenDictionaries message is
// processed.
- if (!mHandler.hasPendingReopenDictionaries()) {
- // May need to reset the contacts dictionary depending on the user settings.
- resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
+ final SettingsValues currentSettingsValues = mSettings.getCurrent();
+ final Suggest suggest = mInputLogic.mSuggest;
+ if (!mHandler.hasPendingReopenDictionaries() && suggest != null) {
+ // May need to reset dictionaries depending on the user settings.
+ final DictionaryFacilitatorForSuggest oldDictionaryFacilitator =
+ suggest.mDictionaryFacilitator;
+ final DictionaryFacilitatorForSuggest dictionaryFacilitator =
+ new DictionaryFacilitatorForSuggest(currentSettingsValues,
+ oldDictionaryFacilitator);
+ // Create Suggest instance with the new dictionary facilitator.
+ mInputLogic.mSuggest = new Suggest(suggest /* oldSuggest */, dictionaryFacilitator);
+ suggest.close();
+ }
+ if (currentSettingsValues.mUsePersonalizedDicts) {
+ if (mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypes()) {
+ PersonalizationDictionarySessionRegistrar.init(this);
+ } else {
+ PersonalizationDictionarySessionRegistrar.close(this);
+ }
+ } else {
+ PersonalizationHelper.removeAllPersonalizedDictionaries(this);
+ PersonalizationDictionarySessionRegistrar.resetAll(this);
}
}
// Note that this method is called from a non-UI thread.
@Override
public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) {
- mIsMainDictionaryAvailable = isMainDictionaryAvailable;
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
if (mainKeyboardView != null) {
mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable);
@@ -609,7 +547,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final Locale switcherSubtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
final String switcherLocaleStr = switcherSubtypeLocale.toString();
final Locale subtypeLocale;
- final String localeStr;
if (TextUtils.isEmpty(switcherLocaleStr)) {
// 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
@@ -619,101 +556,42 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// of knowing anyway.
Log.e(TAG, "System is reporting no current subtype.");
subtypeLocale = getResources().getConfiguration().locale;
- localeStr = subtypeLocale.toString();
} else {
subtypeLocale = switcherSubtypeLocale;
- localeStr = switcherLocaleStr;
}
- final Suggest newSuggest = new Suggest(this /* Context */, subtypeLocale,
- this /* SuggestInitializationListener */);
final SettingsValues settingsValues = mSettings.getCurrent();
+ final DictionaryFacilitatorForSuggest oldDictionaryFacilitator =
+ (mInputLogic.mSuggest == null) ? null : mInputLogic.mSuggest.mDictionaryFacilitator;
+ // Creates new dictionary facilitator for the new locale.
+ final DictionaryFacilitatorForSuggest dictionaryFacilitator =
+ new DictionaryFacilitatorForSuggest(this /* context */, subtypeLocale,
+ settingsValues, this /* DictionaryInitializationListener */,
+ oldDictionaryFacilitator);
+ final Suggest newSuggest = new Suggest(subtypeLocale, dictionaryFacilitator);
if (settingsValues.mCorrectionEnabled) {
newSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold);
}
-
- mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.getInstance().initSuggest(newSuggest);
+ ResearchLogger.getInstance().initDictionary(newSuggest.mDictionaryFacilitator);
}
-
- mUserDictionary = new UserBinaryDictionary(this, localeStr);
- mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
- newSuggest.setUserDictionary(mUserDictionary);
-
- final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
-
- mUserHistoryDictionary = PersonalizationHelper.getUserHistoryDictionary(
- this, localeStr, prefs);
- newSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
- mPersonalizationDictionary = PersonalizationHelper
- .getPersonalizationDictionary(this, localeStr, prefs);
- newSuggest.setPersonalizationDictionary(mPersonalizationDictionary);
- mPersonalizationPredictionDictionary = PersonalizationHelper
- .getPersonalizationPredictionDictionary(this, localeStr, prefs);
- newSuggest.setPersonalizationPredictionDictionary(mPersonalizationPredictionDictionary);
-
- final Suggest oldSuggest = mSuggest;
- resetContactsDictionary(null != oldSuggest ? oldSuggest.getContactsDictionary() : null);
- mSuggest = newSuggest;
+ final Suggest oldSuggest = mInputLogic.mSuggest;
+ mInputLogic.mSuggest = newSuggest;
if (oldSuggest != null) oldSuggest.close();
}
- /**
- * Resets the contacts dictionary in mSuggest according to the user settings.
- *
- * This method takes an optional contacts dictionary to use when the locale hasn't changed
- * since the contacts dictionary can be opened or closed as necessary depending on the settings.
- *
- * @param oldContactsDictionary an optional dictionary to use, or null
- */
- private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
- final Suggest suggest = mSuggest;
- final boolean shouldSetDictionary =
- (null != suggest && mSettings.getCurrent().mUseContactsDict);
-
- final ContactsBinaryDictionary dictionaryToUse;
- if (!shouldSetDictionary) {
- // Make sure the dictionary is closed. If it is already closed, this is a no-op,
- // so it's safe to call it anyways.
- if (null != oldContactsDictionary) oldContactsDictionary.close();
- dictionaryToUse = null;
- } else {
- final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
- if (null != oldContactsDictionary) {
- if (!oldContactsDictionary.mLocale.equals(locale)) {
- // If the locale has changed then recreate the contacts dictionary. This
- // allows locale dependent rules for handling bigram name predictions.
- oldContactsDictionary.close();
- dictionaryToUse = new ContactsBinaryDictionary(this, locale);
- } else {
- // Make sure the old contacts dictionary is opened. If it is already open,
- // this is a no-op, so it's safe to call it anyways.
- oldContactsDictionary.reopen(this);
- dictionaryToUse = oldContactsDictionary;
- }
- } else {
- dictionaryToUse = new ContactsBinaryDictionary(this, locale);
- }
- }
-
- if (null != suggest) {
- suggest.setContactsDictionary(dictionaryToUse);
- }
- }
-
/* package private */ void resetSuggestMainDict() {
final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
- mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */);
- mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
+ mInputLogic.mSuggest.mDictionaryFacilitator.reloadMainDict(this, subtypeLocale,
+ this /* SuggestInitializationListener */);
}
@Override
public void onDestroy() {
- final Suggest suggest = mSuggest;
+ final Suggest suggest = mInputLogic.mSuggest;
if (suggest != null) {
suggest.close();
- mSuggest = null;
+ mInputLogic.mSuggest = null;
}
mSettings.onDestroy();
unregisterReceiver(mReceiver);
@@ -721,30 +599,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
ResearchLogger.getInstance().onDestroy();
}
unregisterReceiver(mDictionaryPackInstallReceiver);
- PersonalizationDictionarySessionRegister.onDestroy(this);
+ PersonalizationDictionarySessionRegistrar.close(this);
LatinImeLogger.commit();
LatinImeLogger.onDestroy();
- if (mInputUpdater != null) {
- mInputUpdater.quitLooper();
- }
super.onDestroy();
}
@Override
public void onConfigurationChanged(final Configuration conf) {
// If orientation changed while predicting, commit the change
- if (mDisplayOrientation != conf.orientation) {
- mDisplayOrientation = conf.orientation;
+ final SettingsValues settingsValues = mSettings.getCurrent();
+ if (settingsValues.mDisplayOrientation != conf.orientation) {
mHandler.startOrientationChanging();
- mConnection.beginBatchEdit();
- commitTyped(LastComposedWord.NOT_A_SEPARATOR);
- mConnection.finishComposingText();
- mConnection.endBatchEdit();
+ mInputLogic.mConnection.beginBatchEdit();
+ mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR);
+ mInputLogic.mConnection.finishComposingText();
+ mInputLogic.mConnection.endBatchEdit();
if (isShowingOptionDialog()) {
mOptionsDialog.dismiss();
}
}
- PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf);
+ PersonalizationDictionarySessionRegistrar.onConfigurationChanged(this, conf);
super.onConfigurationChanged(conf);
}
@@ -760,8 +635,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
.findViewById(android.R.id.extractArea);
mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
- if (mSuggestionStripView != null)
+ if (mSuggestionStripView != null) {
mSuggestionStripView.setListener(this, view);
+ }
if (LatinImeLogger.sVISUALDEBUG) {
mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
}
@@ -849,14 +725,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead");
}
- final PackageInfo packageInfo =
- TargetPackageInfoGetterTask.getCachedPackageInfo(editorInfo.packageName);
- mAppWorkAroundsUtils.setPackageInfo(packageInfo);
- if (null == packageInfo) {
- new TargetPackageInfoGetterTask(this /* context */, this /* listener */)
- .execute(editorInfo.packageName);
- }
-
LatinImeLogger.onStartInputView(editorInfo);
// In landscape mode, this method gets called without the input view being created.
if (mainKeyboardView == null) {
@@ -882,16 +750,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// The app calling setText() has the effect of clearing the composing
// span, so we should reset our state unconditionally, even if restarting is true.
- mEnteredText = null;
- resetComposingState(true /* alsoResetLastComposedWord */);
- mDeleteCount = 0;
- mSpaceState = SPACE_STATE_NONE;
- mRecapitalizeStatus.deactivate();
- mCurrentlyPressedHardwareKeys.clear();
+ mInputLogic.startInput(restarting, editorInfo);
// Note: the following does a round-trip IPC on the main thread: be careful
final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
- final Suggest suggest = mSuggest;
+ final Suggest suggest = mInputLogic.mSuggest;
if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) {
initSuggest();
}
@@ -900,13 +763,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// otherwise it will clear the suggestion strip.
setPunctuationSuggestions();
}
- mSuggestedWords = SuggestedWords.EMPTY;
// Sometimes, while rotating, for some reason the framework tells the app we are not
// connected to it and that means we can't refresh the cache. In this case, schedule a
// refresh later.
+ // TODO[IL]: Can the following be moved to InputLogic#startInput?
final boolean canReachInputConnection;
- if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(editorInfo.initialSelStart,
+ if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
+ editorInfo.initialSelStart, editorInfo.initialSelEnd,
false /* shouldFinishComposition */)) {
// We try resetting the caches up to 5 times before giving up.
mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */);
@@ -919,9 +783,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
canReachInputConnection = true;
}
+ if (isDifferentTextField ||
+ !currentSettingsValues.hasSameOrientation(getResources().getConfiguration())) {
+ loadSettings();
+ }
if (isDifferentTextField) {
mainKeyboardView.closing();
- loadSettings();
currentSettingsValues = mSettings.getCurrent();
if (suggest != null && currentSettingsValues.mCorrectionEnabled) {
@@ -947,16 +814,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
setSuggestionStripShownInternal(
isSuggestionsStripVisible(), /* needsInputViewShown */ false);
- mLastSelectionStart = editorInfo.initialSelStart;
- mLastSelectionEnd = editorInfo.initialSelEnd;
- // 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.
- tryFixLyingCursorPosition();
-
mHandler.cancelUpdateSuggestionStrip();
mHandler.cancelDoubleSpacePeriodTimer();
- mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable);
+ mainKeyboardView.setMainDictionaryAvailability(null != suggest
+ ? suggest.mDictionaryFacilitator.hasMainDictionary() : false);
mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn,
currentSettingsValues.mKeyPreviewPopupDismissDelay);
mainKeyboardView.setSlidingKeyInputPreviewEnabled(
@@ -966,76 +828,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
currentSettingsValues.mGestureTrailEnabled,
currentSettingsValues.mGestureFloatingPreviewTextEnabled);
- initPersonalizationDebugSettings(currentSettingsValues);
-
if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
}
- /**
- * 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
- * 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.
- */
- private void tryFixLyingCursorPosition() {
- final CharSequence textBeforeCursor =
- mConnection.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
- if (null == textBeforeCursor) {
- mLastSelectionStart = mLastSelectionEnd = NOT_A_CURSOR_POSITION;
- } else {
- final int textLength = textBeforeCursor.length();
- if (textLength > mLastSelectionStart
- || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
- && mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
- // It should not be possible to have only one of those variables be
- // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized
- // (simple cursor, no selection) or there is no cursor/we don't know its pos
- final boolean wasEqual = mLastSelectionStart == mLastSelectionEnd;
- mLastSelectionStart = textLength;
- // We can't figure out the value of mLastSelectionEnd :(
- // But at least if it's smaller than mLastSelectionStart something is wrong,
- // and if they used to be equal we also don't want to make it look like there is a
- // selection.
- if (wasEqual || mLastSelectionStart > mLastSelectionEnd) {
- mLastSelectionEnd = mLastSelectionStart;
- }
- }
- }
- }
-
- // Initialization of personalization debug settings. This must be called inside
- // onStartInputView.
- private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) {
- if (mUseOnlyPersonalizationDictionaryForDebug
- != currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug) {
- // Only for debug
- initSuggest();
- mUseOnlyPersonalizationDictionaryForDebug =
- currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug;
- }
-
- if (mBoostPersonalizationDictionaryForDebug !=
- currentSettingsValues.mBoostPersonalizationDictionaryForDebug) {
- // Only for debug
- mBoostPersonalizationDictionaryForDebug =
- currentSettingsValues.mBoostPersonalizationDictionaryForDebug;
- if (mBoostPersonalizationDictionaryForDebug) {
- UserHistoryForgettingCurveUtils.boostMaxFreqForDebug();
- } else {
- UserHistoryForgettingCurveUtils.resetMaxFreqForDebug();
- }
- }
- }
-
- // Callback for the TargetPackageInfoGetterTask
- @Override
- public void onTargetPackageInfoKnown(final PackageInfo info) {
- mAppWorkAroundsUtils.setPackageInfo(info);
- }
-
@Override
public void onWindowHidden() {
super.onWindowHidden();
@@ -1062,12 +857,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Remove pending messages related to update suggestions
mHandler.cancelUpdateSuggestionStrip();
// Should do the following in onFinishInputInternal but until JB MR2 it's not called :(
- if (mWordComposer.isComposingWord()) mConnection.finishComposingText();
- resetComposingState(true /* alsoResetLastComposedWord */);
+ mInputLogic.finishInput();
// Notify ResearchLogger
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart,
- mLastSelectionEnd, getCurrentInputConnection());
+ ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput,
+ mInputLogic.mLastSelectionStart,
+ mInputLogic.mLastSelectionEnd, getCurrentInputConnection());
}
}
@@ -1080,28 +875,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (DEBUG) {
Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
+ ", ose=" + oldSelEnd
- + ", lss=" + mLastSelectionStart
- + ", lse=" + mLastSelectionEnd
+ + ", lss=" + mInputLogic.mLastSelectionStart
+ + ", lse=" + mInputLogic.mLastSelectionEnd
+ ", nss=" + newSelStart
+ ", nse=" + newSelEnd
+ ", cs=" + composingSpanStart
+ ", ce=" + composingSpanEnd);
}
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- final boolean expectingUpdateSelectionFromLogger =
- ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
- ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
+ ResearchLogger.latinIME_onUpdateSelection(mInputLogic.mLastSelectionStart,
+ mInputLogic.mLastSelectionEnd,
oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
- composingSpanEnd, mExpectingUpdateSelection,
- expectingUpdateSelectionFromLogger, mConnection);
- if (expectingUpdateSelectionFromLogger) {
- // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work
- return;
- }
+ composingSpanEnd, mInputLogic.mConnection);
}
- final boolean selectionChanged = mLastSelectionStart != newSelStart
- || mLastSelectionEnd != newSelEnd;
+ final boolean selectionChanged = mInputLogic.mLastSelectionStart != newSelStart
+ || mInputLogic.mLastSelectionEnd != newSelEnd;
// if composingSpanStart and composingSpanEnd are -1, it means there is no composing
// span in the view - we can use that to narrow down whether the cursor was moved
@@ -1116,30 +905,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// TODO: revisit this when LatinIME supports hardware keyboards.
// NOTE: the test harness subclasses LatinIME and overrides isInputViewShown().
// TODO: find a better way to simulate actual execution.
- if (isInputViewShown() && !mExpectingUpdateSelection
- && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) {
- // TAKE CARE: there is a race condition when we enter this test even when the user
- // did not explicitly move the cursor. This happens when typing fast, where two keys
- // turn this flag on in succession and both onUpdateSelection() calls arrive after
- // the second one - the first call successfully avoids this test, but the second one
- // enters. For the moment we rely on noComposingSpan to further reduce the impact.
-
+ if (isInputViewShown() && !mInputLogic.mConnection.isBelatedExpectedUpdate(oldSelStart,
+ newSelStart, oldSelEnd, newSelEnd)) {
// TODO: the following is probably better done in resetEntireInputState().
// it should only happen when the cursor moved, and the very purpose of the
// test below is to narrow down whether this happened or not. Likewise with
// the call to updateShiftState.
// We set this to NONE because after a cursor move, we don't want the space
// state-related special processing to kick in.
- mSpaceState = SPACE_STATE_NONE;
+ mInputLogic.mSpaceState = SpaceState.NONE;
// TODO: is it still necessary to test for composingSpan related stuff?
final boolean selectionChangedOrSafeToReset = selectionChanged
- || (!mWordComposer.isComposingWord()) || noComposingSpan;
+ || (!mInputLogic.mWordComposer.isComposingWord()) || noComposingSpan;
final boolean hasOrHadSelection = (oldSelStart != oldSelEnd
|| newSelStart != newSelEnd);
final int moveAmount = newSelStart - oldSelStart;
if (selectionChangedOrSafeToReset && (hasOrHadSelection
- || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) {
+ || !mInputLogic.mWordComposer.moveCursorByAndReturnIfInsideComposingWord(
+ moveAmount))) {
// If we are composing a word and moving the cursor, we would want to set a
// suggestion span for recorrection to work correctly. Unfortunately, that
// would involve the keyboard committing some new text, which would move the
@@ -1149,14 +933,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Another option would be to send suggestions each time we set the composing
// text, but that is probably too expensive to do, so we decided to leave things
// as is.
- resetEntireInputState(newSelStart);
+ mInputLogic.resetEntireInputState(mSettings.getCurrent(), newSelStart, newSelEnd);
} else {
- // resetEntireInputState calls resetCachesUponCursorMove, but with the second
- // argument as true. But in all cases where we don't reset the entire input state,
- // we still want to tell the rich input connection about the new cursor position so
- // that it can update its caches.
- mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart,
- false /* shouldFinishComposition */);
+ // resetEntireInputState calls resetCachesUponCursorMove, but forcing the
+ // composition to end. But in all cases where we don't reset the entire input
+ // state, we still want to tell the rich input connection about the new cursor
+ // position so that it can update its caches.
+ mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
+ newSelStart, newSelEnd, false /* shouldFinishComposition */);
}
// We moved the cursor. If we are touching a word, we need to resume suggestion,
@@ -1165,14 +949,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mHandler.postResumeSuggestions();
}
// Reset the last recapitalization.
- mRecapitalizeStatus.deactivate();
+ mInputLogic.mRecapitalizeStatus.deactivate();
mKeyboardSwitcher.updateShiftState();
}
- mExpectingUpdateSelection = false;
// Make a note of the cursor position
- mLastSelectionStart = newSelStart;
- mLastSelectionEnd = newSelEnd;
+ mInputLogic.mLastSelectionStart = newSelStart;
+ mInputLogic.mLastSelectionEnd = newSelEnd;
mSubtypeState.currentSubtypeUsed();
}
@@ -1186,7 +969,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
*/
@Override
public void onExtractedTextClicked() {
- if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
+ if (mSettings.getCurrent().isSuggestionsRequested()) {
+ return;
+ }
super.onExtractedTextClicked();
}
@@ -1202,7 +987,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
*/
@Override
public void onExtractedCursorMovement(final int dx, final int dy) {
- if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
+ if (mSettings.getCurrent().isSuggestionsRequested()) {
+ return;
+ }
super.onExtractedCursorMovement(dx, dy);
}
@@ -1256,9 +1043,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
false /* isObsoleteSuggestions */,
false /* isPrediction */);
// When in fullscreen mode, show completions generated by the application
- final boolean isAutoCorrection = false;
- setSuggestedWords(suggestedWords, isAutoCorrection);
- setAutoCorrectionIndicator(isAutoCorrection);
+ setSuggestedWords(suggestedWords);
+ setAutoCorrectionIndicator(false);
setSuggestionStripShown(true);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
@@ -1353,9 +1139,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public boolean onEvaluateFullscreenMode() {
- // Reread resource value here, because this method is called by framework anytime as needed.
- final boolean isFullscreenModeAllowed =
- Settings.readUseFullscreenMode(getResources());
+ // Reread resource value here, because this method is called by the framework as needed.
+ final boolean isFullscreenModeAllowed = Settings.readUseFullscreenMode(getResources());
if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) {
// TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI
// implies NO_FULLSCREEN. However, the framework mistakenly does. i.e. NO_EXTRACT_UI
@@ -1378,138 +1163,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
}
- // This will reset the whole input state to the starting state. It will clear
- // the composing word, reset the last composed word, tell the inputconnection about it.
- private void resetEntireInputState(final int newCursorPosition) {
- final boolean shouldFinishComposition = mWordComposer.isComposingWord();
- resetComposingState(true /* alsoResetLastComposedWord */);
- final SettingsValues settingsValues = mSettings.getCurrent();
- if (settingsValues.mBigramPredictionEnabled) {
- clearSuggestionStrip();
- } else {
- setSuggestedWords(settingsValues.mSuggestPuncList, false);
- }
- mConnection.resetCachesUponCursorMoveAndReturnSuccess(newCursorPosition,
- shouldFinishComposition);
- }
-
- private void resetComposingState(final boolean alsoResetLastComposedWord) {
- mWordComposer.reset();
- if (alsoResetLastComposedWord)
- mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
- }
-
- private void commitTyped(final String separatorString) {
- if (!mWordComposer.isComposingWord()) return;
- final String typedWord = mWordComposer.getTypedWord();
- if (typedWord.length() > 0) {
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode());
- }
- commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD,
- separatorString);
- }
- }
-
// Called from the KeyboardSwitcher which needs to know auto caps state to display
// the right layout.
+ // TODO[IL]: Remove this, pass the input logic to the keyboard switcher instead?
public int getCurrentAutoCapsState() {
- final SettingsValues currentSettingsValues = mSettings.getCurrent();
- if (!currentSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
-
- final EditorInfo ei = getCurrentInputEditorInfo();
- if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
- final int inputType = ei.inputType;
- // Warning: this depends on mSpaceState, which may not be the most current value. If
- // mSpaceState gets updated later, whoever called this may need to be told about it.
- return mConnection.getCursorCapsMode(inputType, currentSettingsValues,
- SPACE_STATE_PHANTOM == mSpaceState);
+ return mInputLogic.getCurrentAutoCapsState(null /* optionalSettingsValues */);
}
+ // Called from the KeyboardSwitcher which needs to know recaps state to display
+ // the right layout.
+ // TODO[IL]: Remove this, pass the input logic to the keyboard switcher instead?
public int getCurrentRecapitalizeState() {
- if (!mRecapitalizeStatus.isActive()
- || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
- // Not recapitalizing at the moment
- return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
- }
- return mRecapitalizeStatus.getCurrentMode();
- }
-
- // Factor in auto-caps and manual caps and compute the current caps mode.
- private int getActualCapsMode() {
- final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode();
- if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
- final int auto = getCurrentAutoCapsState();
- if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
- return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
- }
- if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED;
- return WordComposer.CAPS_MODE_OFF;
- }
-
- private void swapSwapperAndSpace() {
- final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
- // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
- if (lastTwo != null && lastTwo.length() == 2
- && lastTwo.charAt(0) == Constants.CODE_SPACE) {
- mConnection.deleteSurroundingText(2, 0);
- final String text = lastTwo.charAt(1) + " ";
- mConnection.commitText(text, 1);
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text);
- }
- mKeyboardSwitcher.updateShiftState();
- }
+ return mInputLogic.getCurrentRecapitalizeState();
}
- private boolean maybeDoubleSpacePeriod() {
- final SettingsValues currentSettingsValues = mSettings.getCurrent();
- if (!currentSettingsValues.mUseDoubleSpacePeriod) return false;
- if (!mHandler.isAcceptingDoubleSpacePeriod()) return false;
- // We only do this when we see two spaces and an accepted code point before the cursor.
- // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars.
- final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0);
- if (null == lastThree) return false;
- final int length = lastThree.length();
- if (length < 3) return false;
- if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false;
- if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false;
- // We know there are spaces in pos -1 and -2, and we have at least three chars.
- // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space,
- // so this is fine.
- final int firstCodePoint =
- Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ?
- Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3);
- if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) {
- mHandler.cancelDoubleSpacePeriodTimer();
- mConnection.deleteSurroundingText(2, 0);
- final String textToInsert = new String(
- new int[] { currentSettingsValues.mSentenceSeparator, Constants.CODE_SPACE },
- 0, 2);
- mConnection.commitText(textToInsert, 1);
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
- false /* isBatchMode */);
- }
- mKeyboardSwitcher.updateShiftState();
- return true;
- }
- return false;
- }
-
- private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) {
- // TODO: Check again whether there really ain't a better way to check this.
- // TODO: This should probably be language-dependant...
- return Character.isLetterOrDigit(codePoint)
- || codePoint == Constants.CODE_SINGLE_QUOTE
- || codePoint == Constants.CODE_DOUBLE_QUOTE
- || codePoint == Constants.CODE_CLOSING_PARENTHESIS
- || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
- || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
- || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET
- || codePoint == Constants.CODE_PLUS
- || codePoint == Constants.CODE_PERCENT
- || Character.getType(codePoint) == Character.OTHER_SYMBOL;
+ public Locale getCurrentSubtypeLocale() {
+ return mSubtypeSwitcher.getCurrentSubtypeLocale();
}
// Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
@@ -1521,15 +1190,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return;
}
final String wordToEdit;
- if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) {
- wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
+ if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) {
+ wordToEdit = word.toLowerCase(getCurrentSubtypeLocale());
} else {
wordToEdit = word;
}
- mUserDictionary.addWordToUserDictionary(wordToEdit);
+ mInputLogic.mSuggest.mDictionaryFacilitator.addWordToUserDictionary(wordToEdit);
}
- private void onSettingsKeyPressed() {
+ public void displaySettingsDialog() {
if (isShowingOptionDialog()) return;
showSubtypeSelectorAndSettings();
}
@@ -1552,12 +1221,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return mOptionsDialog != null && mOptionsDialog.isShowing();
}
- private void performEditorAction(final int actionId) {
- mConnection.performEditorAction(actionId);
- }
-
// TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
- private void handleLanguageSwitchKey() {
+ public void switchToNextSubtype() {
final IBinder token = getWindow().getWindow().getAttributes().token;
if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) {
mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */);
@@ -1566,394 +1231,41 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mSubtypeState.switchSubtype(token, mRichImm);
}
- private void sendDownUpKeyEvent(final int code) {
- final long eventTime = SystemClock.uptimeMillis();
- mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
- KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
- KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
- mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
- KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
- KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
- }
-
- private void sendKeyCodePoint(final int code) {
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_sendKeyCodePoint(code);
- }
- // TODO: Remove this special handling of digit letters.
- // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
- if (code >= '0' && code <= '9') {
- sendDownUpKeyEvent(code - '0' + KeyEvent.KEYCODE_0);
- return;
- }
-
- if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) {
- // 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.
- sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER);
- } else {
- mConnection.commitText(StringUtils.newSingleCodePointString(code), 1);
- }
- }
-
// Implementation of {@link KeyboardActionListener}.
@Override
public void onCodeInput(final int primaryCode, final int x, final int y) {
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
- }
- final long when = SystemClock.uptimeMillis();
- if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
- mDeleteCount = 0;
- }
- mLastKeyTime = when;
- mConnection.beginBatchEdit();
- final KeyboardSwitcher switcher = mKeyboardSwitcher;
- // The space state depends only on the last character pressed and its own previous
- // state. Here, we revert the space state to neutral if the key is actually modifying
- // the input contents (any non-shift key), which is what we should do for
- // all inputs that do not result in a special state. Each character handling is then
- // free to override the state as they see fit.
- final int spaceState = mSpaceState;
- if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false;
-
- // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
- if (primaryCode != Constants.CODE_SPACE) {
- mHandler.cancelDoubleSpacePeriodTimer();
- }
-
- boolean didAutoCorrect = false;
- switch (primaryCode) {
- case Constants.CODE_DELETE:
- mSpaceState = SPACE_STATE_NONE;
- handleBackspace(spaceState);
- LatinImeLogger.logOnDelete(x, y);
- break;
- case Constants.CODE_SHIFT:
- // Note: Calling back to the keyboard on Shift key is handled in
- // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
- final Keyboard currentKeyboard = switcher.getKeyboard();
- if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
- // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
- // alphabetic shift and shift while in symbol layout.
- handleRecapitalize();
- }
- break;
- case Constants.CODE_CAPSLOCK:
- // Note: Changing keyboard to shift lock state is handled in
- // {@link KeyboardSwitcher#onCodeInput(int)}.
- break;
- case Constants.CODE_SWITCH_ALPHA_SYMBOL:
- // Note: Calling back to the keyboard on symbol key is handled in
- // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
- break;
- case Constants.CODE_SETTINGS:
- onSettingsKeyPressed();
- break;
- case Constants.CODE_SHORTCUT:
- mSubtypeSwitcher.switchToShortcutIME(this);
- break;
- case Constants.CODE_ACTION_NEXT:
- performEditorAction(EditorInfo.IME_ACTION_NEXT);
- break;
- case Constants.CODE_ACTION_PREVIOUS:
- performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
- break;
- case Constants.CODE_LANGUAGE_SWITCH:
- handleLanguageSwitchKey();
- break;
- case Constants.CODE_EMOJI:
- // Note: Switching emoji keyboard is being handled in
- // {@link KeyboardState#onCodeInput(int,int)}.
- break;
- case Constants.CODE_ENTER:
- final EditorInfo editorInfo = getCurrentInputEditorInfo();
- final int imeOptionsActionId =
- InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
- if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
- // Either we have an actionLabel and we should performEditorAction with actionId
- // regardless of its value.
- performEditorAction(editorInfo.actionId);
- } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
- // We didn't have an actionLabel, but we had another action to execute.
- // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
- // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
- // means there should be an action and the app didn't bother to set a specific
- // code for it - presumably it only handles one. It does not have to be treated
- // in any specific way: anything that is not IME_ACTION_NONE should be sent to
- // performEditorAction.
- performEditorAction(imeOptionsActionId);
- } else {
- // No action label, and the action from imeOptions is NONE: this is a regular
- // enter key that should input a carriage return.
- didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
- }
- break;
- case Constants.CODE_SHIFT_ENTER:
- didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
- break;
- default:
- didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState);
- break;
- }
- switcher.onCodeInput(primaryCode);
- // Reset after any single keystroke, except shift, capslock, and symbol-shift
- if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT
- && primaryCode != Constants.CODE_CAPSLOCK
- && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
- mLastComposedWord.deactivate();
- if (Constants.CODE_DELETE != primaryCode) {
- mEnteredText = null;
- }
- mConnection.endBatchEdit();
- }
-
- private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y,
- final int spaceState) {
- mSpaceState = SPACE_STATE_NONE;
- final boolean didAutoCorrect;
- final SettingsValues settingsValues = mSettings.getCurrent();
- if (settingsValues.isWordSeparator(primaryCode)
- || Character.getType(primaryCode) == Character.OTHER_SYMBOL) {
- didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
- } else {
- didAutoCorrect = false;
- if (SPACE_STATE_PHANTOM == spaceState) {
- if (settingsValues.mIsInternal) {
- if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
- LatinImeLoggerUtils.onAutoCorrection(
- "", mWordComposer.getTypedWord(), " ", mWordComposer);
- }
- }
- 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 character at the current cursor position.
- resetEntireInputState(mLastSelectionStart);
- } else {
- commitTyped(LastComposedWord.NOT_A_SEPARATOR);
- }
- }
- final int keyX, keyY;
- final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
- if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
- keyX = x;
- keyY = y;
- } else {
- keyX = Constants.NOT_A_COORDINATE;
- keyY = Constants.NOT_A_COORDINATE;
- }
- handleCharacter(primaryCode, keyX, keyY, spaceState);
- }
- mExpectingUpdateSelection = true;
- return didAutoCorrect;
+ mInputLogic.onCodeInput(primaryCode, x, y, mHandler, mKeyboardSwitcher, mSubtypeSwitcher);
}
// Called from PointerTracker through the KeyboardActionListener interface
@Override
public void onTextInput(final String rawText) {
- mConnection.beginBatchEdit();
- if (mWordComposer.isComposingWord()) {
- commitCurrentAutoCorrection(rawText);
- } else {
- resetComposingState(true /* alsoResetLastComposedWord */);
- }
- mHandler.postUpdateSuggestionStrip();
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS
- && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) {
- ResearchLogger.getInstance().onResearchKeySelected(this);
- return;
- }
- final String text = specificTldProcessingOnTextInput(rawText);
- if (SPACE_STATE_PHANTOM == mSpaceState) {
- promotePhantomSpace();
- }
- mConnection.commitText(text, 1);
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */);
- }
- mConnection.endBatchEdit();
- // Space state must be updated before calling updateShiftState
- mSpaceState = SPACE_STATE_NONE;
+ mInputLogic.onTextInput(mSettings.getCurrent(), rawText, mHandler);
mKeyboardSwitcher.updateShiftState();
mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT);
- mEnteredText = text;
}
@Override
public void onStartBatchInput() {
- mInputUpdater.onStartBatchInput();
- mHandler.cancelUpdateSuggestionStrip();
- mConnection.beginBatchEdit();
- final SettingsValues settingsValues = mSettings.getCurrent();
- if (mWordComposer.isComposingWord()) {
- if (settingsValues.mIsInternal) {
- if (mWordComposer.isBatchMode()) {
- LatinImeLoggerUtils.onAutoCorrection(
- "", mWordComposer.getTypedWord(), " ", mWordComposer);
- }
- }
- final int wordComposerSize = mWordComposer.size();
- // Since isComposingWord() is true, the size is at least 1.
- 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.
- resetEntireInputState(mLastSelectionStart);
- } else if (wordComposerSize <= 1) {
- // We auto-correct the previous (typed, not gestured) string iff it's one character
- // long. The reason for this is, even in the middle of gesture typing, you'll still
- // tap one-letter words and you want them auto-corrected (typically, "i" in English
- // should become "I"). However for any longer word, we assume that the reason for
- // tapping probably is that the word you intend to type is not in the dictionary,
- // so we do not attempt to correct, on the assumption that if that was a dictionary
- // word, the user would probably have gestured instead.
- commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR);
- } else {
- commitTyped(LastComposedWord.NOT_A_SEPARATOR);
- }
- mExpectingUpdateSelection = true;
- }
- final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
- if (Character.isLetterOrDigit(codePointBeforeCursor)
- || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
- mSpaceState = SPACE_STATE_PHANTOM;
- }
- mConnection.endBatchEdit();
- mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+ mInputLogic.onStartBatchInput(mSettings.getCurrent(), mKeyboardSwitcher, mHandler);
}
- static final class InputUpdater implements Handler.Callback {
- private final Handler mHandler;
- private final LatinIME mLatinIme;
- private final Object mLock = new Object();
- private boolean mInBatchInput; // synchronized using {@link #mLock}.
-
- InputUpdater(final LatinIME latinIme) {
- final HandlerThread handlerThread = new HandlerThread(
- InputUpdater.class.getSimpleName());
- handlerThread.start();
- mHandler = new Handler(handlerThread.getLooper(), this);
- mLatinIme = latinIme;
- }
-
- private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1;
- private static final int MSG_GET_SUGGESTED_WORDS = 2;
-
- @Override
- public boolean handleMessage(final Message msg) {
- // TODO: straighten message passing - we don't need two kinds of messages calling
- // each other.
- switch (msg.what) {
- case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
- updateBatchInput((InputPointers)msg.obj, msg.arg2 /* sequenceNumber */);
- break;
- case MSG_GET_SUGGESTED_WORDS:
- mLatinIme.getSuggestedWords(msg.arg1 /* sessionId */,
- msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj);
- break;
- }
- return true;
- }
-
- // Run in the UI thread.
- public void onStartBatchInput() {
- synchronized (mLock) {
- mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
- mInBatchInput = true;
- mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
- SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
- }
- }
-
- // Run in the Handler thread.
- private void updateBatchInput(final InputPointers batchPointers, final int sequenceNumber) {
- synchronized (mLock) {
- if (!mInBatchInput) {
- // Batch input has ended or canceled while the message was being delivered.
- return;
- }
-
- getSuggestedWordsGestureLocked(batchPointers, sequenceNumber,
- new OnGetSuggestedWordsCallback() {
- @Override
- public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
- mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
- suggestedWords, false /* dismissGestureFloatingPreviewText */);
- }
- });
- }
- }
-
- // Run in the UI thread.
- public void onUpdateBatchInput(final InputPointers batchPointers,
- final int sequenceNumber) {
- if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) {
- return;
- }
- mHandler.obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, 0 /* arg1 */,
- sequenceNumber /* arg2 */, batchPointers /* obj */).sendToTarget();
- }
-
- public void onCancelBatchInput() {
- synchronized (mLock) {
- mInBatchInput = false;
- mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
- SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
- }
- }
-
- // Run in the UI thread.
- public void onEndBatchInput(final InputPointers batchPointers) {
- synchronized(mLock) {
- getSuggestedWordsGestureLocked(batchPointers, SuggestedWords.NOT_A_SEQUENCE_NUMBER,
- new OnGetSuggestedWordsCallback() {
- @Override
- public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
- mInBatchInput = false;
- mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords,
- true /* dismissGestureFloatingPreviewText */);
- mLatinIme.mHandler.onEndBatchInput(suggestedWords);
- }
- });
- }
- }
-
- // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
- // be synchronized.
- private void getSuggestedWordsGestureLocked(final InputPointers batchPointers,
- final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
- mLatinIme.mWordComposer.setBatchInputPointers(batchPointers);
- mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE,
- sequenceNumber, new OnGetSuggestedWordsCallback() {
- @Override
- public void onGetSuggestedWords(SuggestedWords suggestedWords) {
- final int suggestionCount = suggestedWords.size();
- if (suggestionCount <= 1) {
- final String mostProbableSuggestion = (suggestionCount == 0) ? null
- : suggestedWords.getWord(0);
- callback.onGetSuggestedWords(
- mLatinIme.getOlderSuggestions(mostProbableSuggestion));
- }
- callback.onGetSuggestedWords(suggestedWords);
- }
- });
- }
+ @Override
+ public void onUpdateBatchInput(final InputPointers batchPointers) {
+ mInputLogic.onUpdateBatchInput(mSettings.getCurrent(), batchPointers, mKeyboardSwitcher);
+ }
- public void getSuggestedWords(final int sessionId, final int sequenceNumber,
- final OnGetSuggestedWordsCallback callback) {
- mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback)
- .sendToTarget();
- }
+ @Override
+ public void onEndBatchInput(final InputPointers batchPointers) {
+ mInputLogic.onEndBatchInput(mSettings.getCurrent(), batchPointers);
+ }
- void quitLooper() {
- mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS);
- mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
- mHandler.getLooper().quit();
- }
+ @Override
+ public void onCancelBatchInput() {
+ mInputLogic.onCancelBatchInput(mHandler);
}
- // This method must run in UI Thread.
+ // This method must run on the UI Thread.
private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
final boolean dismissGestureFloatingPreviewText) {
showSuggestionStrip(suggestedWords);
@@ -1964,102 +1276,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- /* The sequence number member is only used in onUpdateBatchInput. It is increased each time
- * auto-commit happens. The reason we need this is, when auto-commit happens we trim the
- * input pointers that are held in a singleton, and to know how much to trim we rely on the
- * results of the suggestion process that is held in mSuggestedWords.
- * However, the suggestion process is asynchronous, and sometimes we may enter the
- * onUpdateBatchInput method twice without having recomputed suggestions yet, or having
- * received new suggestions generated from not-yet-trimmed input pointers. In this case, the
- * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we
- * remove an unrelated number of pointers (possibly even more than are left in the input
- * pointers, leading to a crash).
- * To avoid that, we increase the sequence number each time we auto-commit and trim the
- * input pointers, and we do not use any suggested words that have been generated with an
- * earlier sequence number.
- */
- private int mAutoCommitSequenceNumber = 1;
- @Override
- public void onUpdateBatchInput(final InputPointers batchPointers) {
- if (mSettings.getCurrent().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(" ", 2);
- batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
- promotePhantomSpace();
- mConnection.commitText(commitParts[0], 0);
- mSpaceState = SPACE_STATE_PHANTOM;
- mKeyboardSwitcher.updateShiftState();
- mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
- ++mAutoCommitSequenceNumber;
- }
- }
- }
- mInputUpdater.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
- }
-
- // This method must run in UI Thread.
- public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) {
- final String batchInputText = suggestedWords.isEmpty()
- ? null : suggestedWords.getWord(0);
- if (TextUtils.isEmpty(batchInputText)) {
- return;
- }
- mConnection.beginBatchEdit();
- if (SPACE_STATE_PHANTOM == mSpaceState) {
- promotePhantomSpace();
- }
- if (mSettings.getCurrent().mPhraseGestureEnabled) {
- // Find the last space
- final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
- if (0 != indexOfLastSpace) {
- mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
- showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture());
- }
- final String lastWord = batchInputText.substring(indexOfLastSpace);
- mWordComposer.setBatchInputWord(lastWord);
- mConnection.setComposingText(lastWord, 1);
- } else {
- mWordComposer.setBatchInputWord(batchInputText);
- mConnection.setComposingText(batchInputText, 1);
- }
- mExpectingUpdateSelection = true;
- mConnection.endBatchEdit();
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords);
- }
- // Space state must be updated before calling updateShiftState
- mSpaceState = SPACE_STATE_PHANTOM;
- mKeyboardSwitcher.updateShiftState();
- }
-
- @Override
- public void onEndBatchInput(final InputPointers batchPointers) {
- mInputUpdater.onEndBatchInput(batchPointers);
- }
-
- private String specificTldProcessingOnTextInput(final String text) {
- if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
- || !Character.isLetter(text.charAt(1))) {
- // Not a tld: do nothing.
- return text;
- }
- // We have a TLD (or something that looks like this): make sure we don't add
- // a space even if currently in phantom mode.
- mSpaceState = SPACE_STATE_NONE;
- // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code
- final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
- if (lastOne != null && lastOne.length() == 1
- && lastOne.charAt(0) == Constants.CODE_PERIOD) {
- return text.substring(1);
- } else {
- return text;
- }
- }
-
// Called from PointerTracker through the KeyboardActionListener interface
@Override
public void onFinishSlidingInput() {
@@ -2074,393 +1290,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Nothing to do so far.
}
- @Override
- public void onCancelBatchInput() {
- mInputUpdater.onCancelBatchInput();
- }
-
- private void handleBackspace(final int spaceState) {
- // We revert these in this method if the deletion doesn't happen.
- mDeleteCount++;
- mExpectingUpdateSelection = true;
-
- // In many cases, we may have to put the keyboard in auto-shift state again. However
- // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
- // during key repeat.
- mHandler.postUpdateShiftState();
-
- if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
- // If we are in the middle of a recorrection, we need to commit the recorrection
- // first so that we can remove the character at the current cursor position.
- resetEntireInputState(mLastSelectionStart);
- // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
- }
- if (mWordComposer.isComposingWord()) {
- if (mWordComposer.isBatchMode()) {
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- final String word = mWordComposer.getTypedWord();
- ResearchLogger.latinIME_handleBackspace_batch(word, 1);
- }
- final String rejectedSuggestion = mWordComposer.getTypedWord();
- mWordComposer.reset();
- mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
- } else {
- mWordComposer.deleteLast();
- }
- mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
- mHandler.postUpdateSuggestionStrip();
- if (!mWordComposer.isComposingWord()) {
- // If we just removed the last character, auto-caps mode may have changed so we
- // need to re-evaluate.
- mKeyboardSwitcher.updateShiftState();
- }
- } else {
- final SettingsValues currentSettings = mSettings.getCurrent();
- if (mLastComposedWord.canRevertCommit()) {
- if (currentSettings.mIsInternal) {
- LatinImeLoggerUtils.onAutoCorrectionCancellation();
- }
- revertCommit();
- return;
- }
- if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
- // Cancel multi-character input: remove the text we just entered.
- // This is triggered on backspace after a key that inputs multiple characters,
- // like the smiley key or the .com key.
- mConnection.deleteSurroundingText(mEnteredText.length(), 0);
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
- }
- 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
- // reverting any autocorrect at this point. So we can safely return.
- return;
- }
- if (SPACE_STATE_DOUBLE == spaceState) {
- mHandler.cancelDoubleSpacePeriodTimer();
- if (mConnection.revertDoubleSpacePeriod()) {
- // No need to reset mSpaceState, it has already be done (that's why we
- // receive it as a parameter)
- return;
- }
- } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
- if (mConnection.revertSwapPunctuation()) {
- // Likewise
- return;
- }
- }
-
- // 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 (mLastSelectionStart != mLastSelectionEnd) {
- // If there is a selection, remove it.
- final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
- mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
- // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
- // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
- // but we want to set it right away to avoid it being used with the wrong values
- // later (typically, in a subsequent press on backspace).
- mLastSelectionEnd = mLastSelectionStart;
- mConnection.deleteSurroundingText(numCharsDeleted, 0);
- } else {
- // There is no selection, just delete one character.
- if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
- // This should never happen.
- Log.e(TAG, "Backspace when we don't know the selection position");
- }
- if (mAppWorkAroundsUtils.isBeforeJellyBean() ||
- currentSettings.mInputAttributes.isTypeNull()) {
- // There are two 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.
- sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
- if (mDeleteCount > DELETE_ACCELERATE_AT) {
- sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
- }
- } else {
- final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
- if (codePointBeforeCursor == Constants.NOT_A_CODE) {
- // Nothing to delete before the cursor. We have to revert the deletion
- // states that were updated at the beginning of this method.
- mDeleteCount--;
- mExpectingUpdateSelection = false;
- return;
- }
- final int lengthToDelete =
- Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
- mConnection.deleteSurroundingText(lengthToDelete, 0);
- if (mDeleteCount > DELETE_ACCELERATE_AT) {
- final int codePointBeforeCursorToDeleteAgain =
- mConnection.getCodePointBeforeCursor();
- if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
- final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
- codePointBeforeCursorToDeleteAgain) ? 2 : 1;
- mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
- }
- }
- }
- }
- if (currentSettings.isSuggestionsRequested(mDisplayOrientation)
- && currentSettings.mCurrentLanguageHasSpaces) {
- restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
- }
- // We just removed a character. We need to update the auto-caps state.
- mKeyboardSwitcher.updateShiftState();
- }
- }
-
- /*
- * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
- */
- private boolean maybeStripSpace(final int code,
- final int spaceState, final boolean isFromSuggestionStrip) {
- if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
- mConnection.removeTrailingSpace();
- return false;
- }
- if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
- && isFromSuggestionStrip) {
- final SettingsValues currentSettings = mSettings.getCurrent();
- if (currentSettings.isUsuallyPrecededBySpace(code)) return false;
- if (currentSettings.isUsuallyFollowedBySpace(code)) return true;
- mConnection.removeTrailingSpace();
- }
- return false;
- }
-
- private void handleCharacter(final int primaryCode, final int x,
- final int y, final int spaceState) {
- // TODO: refactor this method to stop flipping isComposingWord around all the time, and
- // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
- // which has the same name as other handle* methods but is not the same.
- boolean isComposingWord = mWordComposer.isComposingWord();
-
- // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
- // See onStartBatchInput() to see how to do it.
- final SettingsValues currentSettings = mSettings.getCurrent();
- if (SPACE_STATE_PHANTOM == spaceState && !currentSettings.isWordConnector(primaryCode)) {
- if (isComposingWord) {
- // Sanity check
- throw new RuntimeException("Should not be composing here");
- }
- promotePhantomSpace();
- }
-
- 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 character at the current cursor position.
- resetEntireInputState(mLastSelectionStart);
- isComposingWord = false;
- }
- // We want to find out whether to start composing a new word with this character. If so,
- // we need to reset the composing state and switch isComposingWord. The order of the
- // tests is important for good performance.
- // We only start composing if we're not already composing.
- if (!isComposingWord
- // We only start composing if this is a word code point. Essentially that means it's a
- // a letter or a word connector.
- && currentSettings.isWordCodePoint(primaryCode)
- // We never go into composing state if suggestions are not requested.
- && currentSettings.isSuggestionsRequested(mDisplayOrientation) &&
- // In languages with spaces, we only start composing a word when we are not already
- // touching a word. In languages without spaces, the above conditions are sufficient.
- (!mConnection.isCursorTouchingWord(currentSettings)
- || !currentSettings.mCurrentLanguageHasSpaces)) {
- // Reset entirely the composing state anyway, then start composing a new word unless
- // the character is a single quote or a dash. The idea here is, single quote and dash
- // are not separators and they should be treated as normal characters, except in the
- // first position where they should not start composing a word.
- isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode
- && Constants.CODE_DASH != primaryCode);
- // Here we don't need to reset the last composed word. It will be reset
- // when we commit this one, if we ever do; if on the other hand we backspace
- // it entirely and resume suggestions on the previous word, we'd like to still
- // have touch coordinates for it.
- resetComposingState(false /* alsoResetLastComposedWord */);
- }
- if (isComposingWord) {
- final int keyX, keyY;
- if (Constants.isValidCoordinate(x) && Constants.isValidCoordinate(y)) {
- final KeyDetector keyDetector =
- mKeyboardSwitcher.getMainKeyboardView().getKeyDetector();
- keyX = keyDetector.getTouchX(x);
- keyY = keyDetector.getTouchY(y);
- } else {
- keyX = x;
- keyY = y;
- }
- mWordComposer.add(primaryCode, keyX, keyY);
- // If it's the first letter, make note of auto-caps state
- if (mWordComposer.size() == 1) {
- mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
- }
- mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
- } else {
- final boolean swapWeakSpace = maybeStripSpace(primaryCode,
- spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
-
- sendKeyCodePoint(primaryCode);
-
- if (swapWeakSpace) {
- swapSwapperAndSpace();
- mSpaceState = SPACE_STATE_WEAK;
- }
- // In case the "add to dictionary" hint was still displayed.
- if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint();
- }
- mHandler.postUpdateSuggestionStrip();
- if (currentSettings.mIsInternal) {
- LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y);
- }
- }
-
- private void handleRecapitalize() {
- if (mLastSelectionStart == mLastSelectionEnd) return; // No selection
- // If we have a recapitalize in progress, use it; otherwise, create a new one.
- if (!mRecapitalizeStatus.isActive()
- || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
- final CharSequence selectedText =
- mConnection.getSelectedText(0 /* flags, 0 for no styles */);
- if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
- final SettingsValues currentSettings = mSettings.getCurrent();
- mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
- selectedText.toString(), currentSettings.mLocale,
- currentSettings.mWordSeparators);
- // We trim leading and trailing whitespace.
- mRecapitalizeStatus.trim();
- // Trimming the object may have changed the length of the string, and we need to
- // reposition the selection handles accordingly. As this result in an IPC call,
- // only do it if it's actually necessary, in other words if the recapitalize status
- // is not set at the same place as before.
- if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
- mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
- mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
- }
- }
- mConnection.finishComposingText();
- mRecapitalizeStatus.rotate();
- final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
- mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
- mConnection.deleteSurroundingText(numCharsDeleted, 0);
- mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
- mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
- mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
- mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
- // Match the keyboard to the new state.
- mKeyboardSwitcher.updateShiftState();
- }
-
- // Returns true if we do an autocorrection, false otherwise.
- private boolean handleSeparator(final int primaryCode, final int x, final int y,
- final int spaceState) {
- boolean didAutoCorrect = false;
- final SettingsValues currentSettings = mSettings.getCurrent();
- // We avoid sending spaces in languages without spaces if we were composing.
- final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode
- && !currentSettings.mCurrentLanguageHasSpaces && 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 separator at the current cursor position.
- resetEntireInputState(mLastSelectionStart);
- }
- if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing
- if (currentSettings.mCorrectionEnabled) {
- final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
- : StringUtils.newSingleCodePointString(primaryCode);
- commitCurrentAutoCorrection(separator);
- didAutoCorrect = true;
- } else {
- commitTyped(StringUtils.newSingleCodePointString(primaryCode));
- }
- }
-
- final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
- Constants.SUGGESTION_STRIP_COORDINATE == x);
-
- if (SPACE_STATE_PHANTOM == spaceState &&
- currentSettings.isUsuallyPrecededBySpace(primaryCode)) {
- promotePhantomSpace();
- }
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
- }
-
- if (!shouldAvoidSendingCode) {
- sendKeyCodePoint(primaryCode);
- }
-
- if (Constants.CODE_SPACE == primaryCode) {
- if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
- if (maybeDoubleSpacePeriod()) {
- mSpaceState = SPACE_STATE_DOUBLE;
- } else if (!isShowingPunctuationList()) {
- mSpaceState = SPACE_STATE_WEAK;
- }
- }
-
- mHandler.startDoubleSpacePeriodTimer();
- mHandler.postUpdateSuggestionStrip();
- } else {
- if (swapWeakSpace) {
- swapSwapperAndSpace();
- mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
- } else if (SPACE_STATE_PHANTOM == spaceState
- && currentSettings.isUsuallyFollowedBySpace(primaryCode)) {
- // If we are in phantom space state, and the user presses a separator, we want to
- // stay in phantom space state so that the next keypress has a chance to add the
- // space. For example, if I type "Good dat", pick "day" from the suggestion strip
- // then insert a comma and go on to typing the next word, I want the space to be
- // inserted automatically before the next word, the same way it is when I don't
- // input the comma.
- // The case is a little different if the separator is a space stripper. Such a
- // separator does not normally need a space on the right (that's the difference
- // between swappers and strippers), so we should not stay in phantom space state if
- // the separator is a stripper. Hence the additional test above.
- mSpaceState = SPACE_STATE_PHANTOM;
- }
-
- // Set punctuation right away. onUpdateSelection will fire but tests whether it is
- // already displayed or not, so it's okay.
- setPunctuationSuggestions();
- }
- if (currentSettings.mIsInternal) {
- LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y);
- }
-
- mKeyboardSwitcher.updateShiftState();
- return didAutoCorrect;
- }
-
- private CharSequence getTextWithUnderline(final String text) {
- return mIsAutoCorrectionIndicatorOn
- ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
- : text;
- }
-
- private void handleClose() {
- // TODO: Verify that words are logged properly when IME is closed.
- commitTyped(LastComposedWord.NOT_A_SEPARATOR);
- requestHideSelf(0);
- final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
- if (mainKeyboardView != null) {
- mainKeyboardView.closing();
- }
- }
-
- // TODO: make this private
+ // TODO[IL]: Move this to InputLogic and make it private
// Outside LatinIME, only used by the test suite.
@UsedForTesting
- boolean isShowingPunctuationList() {
- if (mSuggestedWords == null) return false;
- return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords;
+ public boolean isShowingPunctuationList() {
+ if (mInputLogic.mSuggestedWords == null) return false;
+ return mSettings.getCurrent().mSuggestPuncList == mInputLogic.mSuggestedWords;
}
- private boolean isSuggestionsStripVisible() {
+ // TODO[IL]: Define a clear interface for this
+ public boolean isSuggestionsStripVisible() {
final SettingsValues currentSettings = mSettings.getCurrent();
if (mSuggestionStripView == null)
return false;
@@ -2468,81 +1307,54 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return true;
if (null == currentSettings)
return false;
- if (!currentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
+ if (!currentSettings.isSuggestionStripVisible())
return false;
if (currentSettings.isApplicationSpecifiedCompletionsOn())
return true;
- return currentSettings.isSuggestionsRequested(mDisplayOrientation);
+ return currentSettings.isSuggestionsRequested();
+ }
+
+ public void dismissAddToDictionaryHint() {
+ if (null != mSuggestionStripView) {
+ mSuggestionStripView.dismissAddToDictionaryHint();
+ }
}
- private void clearSuggestionStrip() {
- setSuggestedWords(SuggestedWords.EMPTY, false);
+ // TODO[IL]: Define a clear interface for this
+ public void clearSuggestionStrip() {
+ setSuggestedWords(SuggestedWords.EMPTY);
setAutoCorrectionIndicator(false);
}
- private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) {
- mSuggestedWords = words;
+ // TODO[IL]: Define a clear interface for this
+ public void setSuggestedWords(final SuggestedWords words) {
+ mInputLogic.mSuggestedWords = words;
if (mSuggestionStripView != null) {
mSuggestionStripView.setSuggestions(words);
- mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
+ mKeyboardSwitcher.onAutoCorrectionStateChanged(words.mWillAutoCorrect);
}
}
private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
// Put a blue underline to a word in TextView which will be auto-corrected.
- if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
- && mWordComposer.isComposingWord()) {
- mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
+ if (mInputLogic.mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
+ && mInputLogic.mWordComposer.isComposingWord()) {
+ mInputLogic.mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
final CharSequence textWithUnderline =
- getTextWithUnderline(mWordComposer.getTypedWord());
+ mInputLogic.getTextWithUnderline(mInputLogic.mWordComposer.getTypedWord());
// TODO: when called from an updateSuggestionStrip() call that results from a posted
// message, this is called outside any batch edit. Potentially, this may result in some
// janky flickering of the screen, although the display speed makes it unlikely in
// the practice.
- mConnection.setComposingText(textWithUnderline, 1);
- }
- }
-
- private void updateSuggestionStrip() {
- mHandler.cancelUpdateSuggestionStrip();
- final SettingsValues currentSettings = mSettings.getCurrent();
-
- // Check if we have a suggestion engine attached.
- if (mSuggest == null
- || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
- if (mWordComposer.isComposingWord()) {
- Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
- + "requested!");
- }
- return;
- }
-
- if (!mWordComposer.isComposingWord() && !currentSettings.mBigramPredictionEnabled) {
- setPunctuationSuggestions();
- return;
- }
-
- final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>();
- getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING,
- SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
- @Override
- public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
- holder.set(suggestedWords);
- }
- }
- );
-
- // This line may cause the current thread to wait.
- final SuggestedWords suggestedWords = holder.get(null, GET_SUGGESTED_WORDS_TIMEOUT);
- if (suggestedWords != null) {
- showSuggestionStrip(suggestedWords);
+ mInputLogic.mConnection.setComposingText(textWithUnderline, 1);
}
}
- private void getSuggestedWords(final int sessionId, final int sequenceNumber,
+ // TODO[IL]: Move this out of LatinIME.
+ public void getSuggestedWords(final int sessionId, final int sequenceNumber,
final OnGetSuggestedWordsCallback callback) {
final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
- final Suggest suggest = mSuggest;
+ final Suggest suggest = mInputLogic.mSuggest;
if (keyboard == null || suggest == null) {
callback.onGetSuggestedWords(SuggestedWords.EMPTY);
return;
@@ -2552,44 +1364,44 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// should just skip whitespace if any, so 1.
final SettingsValues currentSettings = mSettings.getCurrent();
final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
- final String prevWord;
- if (currentSettings.mCurrentLanguageHasSpaces) {
- // If we are typing in a language with spaces we can just look up the previous
- // word from textview.
- prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators,
- mWordComposer.isComposingWord() ? 2 : 1);
- } else {
- prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
- : mLastComposedWord.mCommittedWord;
+
+ if (DEBUG) {
+ if (mInputLogic.mWordComposer.isComposingWord()
+ || mInputLogic.mWordComposer.isBatchMode()) {
+ final String previousWord
+ = mInputLogic.mWordComposer.getPreviousWordForSuggestion();
+ // TODO: this is for checking consistency with older versions. Remove this when
+ // we are confident this is stable.
+ // We're checking the previous word in the text field against the memorized previous
+ // word. If we are composing a word we should have the second word before the cursor
+ // memorized, otherwise we should have the first.
+ final String rereadPrevWord = mInputLogic.getNthPreviousWordForSuggestion(
+ currentSettings, mInputLogic.mWordComposer.isComposingWord() ? 2 : 1);
+ if (!TextUtils.equals(previousWord, rereadPrevWord)) {
+ throw new RuntimeException("Unexpected previous word: "
+ + previousWord + " <> " + rereadPrevWord);
+ }
+ }
}
- suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
+ suggest.getSuggestedWords(mInputLogic.mWordComposer,
+ mInputLogic.mWordComposer.getPreviousWordForSuggestion(),
+ keyboard.getProximityInfo(),
currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
additionalFeaturesOptions, sessionId, sequenceNumber, callback);
}
- private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId,
- final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
- mInputUpdater.getSuggestedWords(sessionId, sequenceNumber,
- new OnGetSuggestedWordsCallback() {
- @Override
- public void onGetSuggestedWords(SuggestedWords suggestedWords) {
- callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions(
- mWordComposer.getTypedWord(), suggestedWords));
- }
- });
- }
-
- private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
+ // TODO[IL]: Move this to InputLogic
+ public SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
final SuggestedWords suggestedWords) {
// TODO: consolidate this into getSuggestedWords
// We update the suggestion strip only when we have some suggestions to show, i.e. when
// the suggestion count is > 1; else, we leave the old suggestions, with the typed word
- // replaced with the new one. However, when the word is a dictionary word, or when the
- // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the
- // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to
- // revert to suggestions - although it is unclear how we can come here if it's displayed.
+ // replaced with the new one. However, when the length of the typed word is 1 or 0 (after
+ // a deletion typically), we do want to remove the old suggestions. Also, if we are showing
+ // the "add to dictionary" hint, we need to revert to suggestions - although it is unclear
+ // how we can come here if it's displayed.
if (suggestedWords.size() > 1 || typedWord.length() <= 1
- || suggestedWords.mTypedWordValid || null == mSuggestionStripView
+ || null == mSuggestionStripView
|| mSuggestionStripView.isShowingAddToDictionaryHint()) {
return suggestedWords;
} else {
@@ -2598,7 +1410,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
private SuggestedWords getOlderSuggestions(final String typedWord) {
- SuggestedWords previousSuggestedWords = mSuggestedWords;
+ SuggestedWords previousSuggestedWords = mInputLogic.mSuggestedWords;
if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) {
previousSuggestedWords = SuggestedWords.EMPTY;
}
@@ -2616,19 +1428,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
false /* isPrediction */);
}
- private void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) {
- if (suggestedWords.isEmpty()) return;
- final String autoCorrection;
- if (suggestedWords.mWillAutoCorrect) {
- autoCorrection = suggestedWords.getWord(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 = typedWord;
- }
- mWordComposer.setAutoCorrection(autoCorrection);
- }
-
private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
final String typedWord) {
if (suggestedWords.isEmpty()) {
@@ -2637,17 +1436,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
clearSuggestionStrip();
return;
}
- setAutoCorrection(suggestedWords, typedWord);
- final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
- setSuggestedWords(suggestedWords, isAutoCorrection);
- setAutoCorrectionIndicator(isAutoCorrection);
+ final String autoCorrection;
+ if (suggestedWords.mWillAutoCorrect) {
+ autoCorrection = suggestedWords.getWord(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 = typedWord;
+ }
+ mInputLogic.mWordComposer.setAutoCorrection(autoCorrection);
+ setSuggestedWords(suggestedWords);
+ setAutoCorrectionIndicator(suggestedWords.mWillAutoCorrect);
setSuggestionStripShown(isSuggestionsStripVisible());
// An auto-correction is available, cache it in accessibility code so
// we can be speak it if the user touches a key that will insert it.
AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord);
}
- private void showSuggestionStrip(final SuggestedWords suggestedWords) {
+ // TODO[IL]: Define a clean interface for this
+ public void showSuggestionStrip(final SuggestedWords suggestedWords) {
if (suggestedWords.isEmpty()) {
clearSuggestionStrip();
return;
@@ -2656,51 +1463,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD));
}
- private void commitCurrentAutoCorrection(final String separator) {
- // Complete any pending suggestions query first
- if (mHandler.hasPendingUpdateSuggestions()) {
- updateSuggestionStrip();
- }
- final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
- final String typedWord = mWordComposer.getTypedWord();
- final String autoCorrection = (typedAutoCorrection != null)
- ? typedAutoCorrection : typedWord;
- if (autoCorrection != null) {
- if (TextUtils.isEmpty(typedWord)) {
- throw new RuntimeException("We have an auto-correction but the typed word "
- + "is empty? Impossible! I must commit suicide.");
- }
- if (mSettings.isInternal()) {
- LatinImeLoggerUtils.onAutoCorrection(
- typedWord, autoCorrection, separator, mWordComposer);
- }
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- final SuggestedWords suggestedWords = mSuggestedWords;
- ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
- separator, mWordComposer.isBatchMode(), suggestedWords);
- }
- mExpectingUpdateSelection = true;
- commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
- separator);
- if (!typedWord.equals(autoCorrection)) {
- // This will make the correction flash for a short while as a visual clue
- // to the user that auto-correction happened. It has no other effect; in particular
- // note that this won't affect the text inside the text field AT ALL: it only makes
- // the segment of text starting at the supplied index and running for the length
- // of the auto-correction flash. At this moment, the "typedWord" argument is
- // ignored by TextView.
- mConnection.commitCorrection(
- new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
- typedWord, autoCorrection));
- }
- }
- }
-
// Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
// interface
@Override
public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) {
- final SuggestedWords suggestedWords = mSuggestedWords;
+ final SuggestedWords suggestedWords = mInputLogic.mSuggestedWords;
final String suggestion = suggestionInfo.mWord;
// If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
if (suggestion.length() == 1 && isShowingPunctuationList()) {
@@ -2718,70 +1485,71 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return;
}
- mConnection.beginBatchEdit();
+ mInputLogic.mConnection.beginBatchEdit();
final SettingsValues currentSettings = mSettings.getCurrent();
- if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0
+ if (SpaceState.PHANTOM == mInputLogic.mSpaceState && suggestion.length() > 0
// In the batch input mode, a manually picked suggested word should just replace
// the current batch input text and there is no need for a phantom space.
- && !mWordComposer.isBatchMode()) {
+ && !mInputLogic.mWordComposer.isBatchMode()) {
final int firstChar = Character.codePointAt(suggestion, 0);
if (!currentSettings.isWordSeparator(firstChar)
|| currentSettings.isUsuallyPrecededBySpace(firstChar)) {
- promotePhantomSpace();
+ mInputLogic.promotePhantomSpace(currentSettings);
}
}
if (currentSettings.isApplicationSpecifiedCompletionsOn()
&& mApplicationSpecifiedCompletions != null
&& index >= 0 && index < mApplicationSpecifiedCompletions.length) {
- mSuggestedWords = SuggestedWords.EMPTY;
+ mInputLogic.mSuggestedWords = SuggestedWords.EMPTY;
if (mSuggestionStripView != null) {
mSuggestionStripView.clear();
}
mKeyboardSwitcher.updateShiftState();
- resetComposingState(true /* alsoResetLastComposedWord */);
+ mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */);
final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
- mConnection.commitCompletion(completionInfo);
- mConnection.endBatchEdit();
+ mInputLogic.mConnection.commitCompletion(completionInfo);
+ mInputLogic.mConnection.endBatchEdit();
return;
}
// We need to log before we commit, because the word composer will store away the user
// typed word.
- final String replacedWord = mWordComposer.getTypedWord();
+ final String replacedWord = mInputLogic.mWordComposer.getTypedWord();
LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords);
- mExpectingUpdateSelection = true;
- commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
- LastComposedWord.NOT_A_SEPARATOR);
+ mInputLogic.commitChosenWord(currentSettings, suggestion,
+ LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion,
- mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind,
- suggestionInfo.mSourceDict.mDictType);
+ mInputLogic.mWordComposer.isBatchMode(), suggestionInfo.mScore,
+ suggestionInfo.mKind, suggestionInfo.mSourceDict.mDictType);
}
- mConnection.endBatchEdit();
+ mInputLogic.mConnection.endBatchEdit();
// Don't allow cancellation of manual pick
- mLastComposedWord.deactivate();
+ mInputLogic.mLastComposedWord.deactivate();
// Space state must be updated before calling updateShiftState
- mSpaceState = SPACE_STATE_PHANTOM;
+ mInputLogic.mSpaceState = SpaceState.PHANTOM;
mKeyboardSwitcher.updateShiftState();
// 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).
// Please note that if mSuggest is null, it means that everything is off: suggestion
// and correction, so we shouldn't try to show the hint
- final Suggest suggest = mSuggest;
+ final Suggest suggest = mInputLogic.mSuggest;
final boolean showingAddToDictionaryHint =
(SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind
|| SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
&& suggest != null
// If the suggestion is not in the dictionary, the hint should be shown.
- && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true);
+ && !suggest.mDictionaryFacilitator.isValidWord(suggestion,
+ true /* ignoreCase */);
if (currentSettings.mIsInternal) {
LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE,
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
}
- if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
+ if (showingAddToDictionaryHint
+ && suggest.mDictionaryFacilitator.isUserDictionaryEnabled()) {
mSuggestionStripView.showAddToDictionaryHint(
suggestion, currentSettings.mHintToSaveText);
} else {
@@ -2790,163 +1558,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- /**
- * Commits the chosen word to the text field and saves it for later retrieval.
- */
- private void commitChosenWord(final String chosenWord, final int commitType,
- final String separatorString) {
- final SuggestedWords suggestedWords = mSuggestedWords;
- mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
- this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
- // Add the word to the user history dictionary
- final String prevWord = addToUserHistoryDictionary(chosenWord);
- // 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, chosenWord, separatorString,
- prevWord);
- }
-
- private void setPunctuationSuggestions() {
+ // TODO[IL]: Define a clean interface for this
+ public void setPunctuationSuggestions() {
final SettingsValues currentSettings = mSettings.getCurrent();
if (currentSettings.mBigramPredictionEnabled) {
clearSuggestionStrip();
} else {
- setSuggestedWords(currentSettings.mSuggestPuncList, false);
+ setSuggestedWords(currentSettings.mSuggestPuncList);
}
setAutoCorrectionIndicator(false);
setSuggestionStripShown(isSuggestionsStripVisible());
}
- private String addToUserHistoryDictionary(final String suggestion) {
- if (TextUtils.isEmpty(suggestion)) return null;
- final Suggest suggest = mSuggest;
- if (suggest == null) return null;
-
- // If correction is not enabled, we don't add words to the user history dictionary.
- // That's to avoid unintended additions in some sensitive fields, or fields that
- // expect to receive non-words.
- final SettingsValues currentSettings = mSettings.getCurrent();
- if (!currentSettings.mCorrectionEnabled) return null;
-
- final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
- if (userHistoryDictionary == null) return null;
-
- final String prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, 2);
- final String secondWord;
- if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
- secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
- } else {
- secondWord = suggestion;
- }
- // 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 int maxFreq = AutoCorrectionUtils.getMaxFrequency(
- suggest.getUnigramDictionaries(), suggestion);
- if (maxFreq == 0) return null;
- userHistoryDictionary.addToDictionary(prevWord, secondWord, maxFreq > 0);
- return prevWord;
- }
-
- private boolean isResumableWord(final String word, final SettingsValues settings) {
- final int firstCodePoint = word.codePointAt(0);
- return settings.isWordCodePoint(firstCodePoint)
- && Constants.CODE_SINGLE_QUOTE != firstCodePoint
- && Constants.CODE_DASH != firstCodePoint;
- }
-
- /**
- * Check if the cursor is touching a word. If so, restart suggestions on this word, else
- * do nothing.
- */
- private void restartSuggestionsOnWordTouchedByCursor() {
- // HACK: We may want to special-case some apps that exhibit bad behavior in case of
- // recorrection. This is a temporary, stopgap measure that will be removed later.
- // TODO: remove this.
- if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return;
- // A simple way to test for support from the TextView.
- if (!isSuggestionsStripVisible()) return;
- // Recorrection is not supported in languages without spaces because we don't know
- // how to segment them yet.
- if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return;
- // If the cursor is not touching a word, or if there is a selection, return right away.
- if (mLastSelectionStart != mLastSelectionEnd) return;
- // If we don't know the cursor location, return.
- if (mLastSelectionStart < 0) return;
- final SettingsValues currentSettings = mSettings.getCurrent();
- if (!mConnection.isCursorTouchingWord(currentSettings)) return;
- final TextRange range = mConnection.getWordRangeAtCursor(currentSettings.mWordSeparators,
- 0 /* additionalPrecedingWordsCount */);
- if (null == range) return; // Happens if we don't have an input connection at all
- if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out.
- // If for some strange reason (editor bug or so) we measure the text before the cursor as
- // longer than what the entire text is supposed to be, the safe thing to do is bail out.
- final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor();
- if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return;
- final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
- final String typedWord = range.mWord.toString();
- if (!isResumableWord(typedWord, currentSettings)) return;
- int i = 0;
- for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) {
- for (final String s : span.getSuggestions()) {
- ++i;
- if (!TextUtils.equals(s, typedWord)) {
- suggestions.add(new SuggestedWordInfo(s,
- SuggestionStripView.MAX_SUGGESTIONS - i,
- SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED,
- SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
- SuggestedWordInfo.NOT_A_CONFIDENCE
- /* autoCommitFirstWordConfidence */));
- }
- }
- }
- mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
- mWordComposer.setCursorPositionWithinWord(
- typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
- mConnection.setComposingRegion(
- mLastSelectionStart - numberOfCharsInWordBeforeCursor,
- mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor());
- if (suggestions.isEmpty()) {
- // We come here if there weren't any suggestion spans on this word. We will try to
- // compute suggestions for it instead.
- mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING,
- SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
- @Override
- public void onGetSuggestedWords(
- final SuggestedWords suggestedWordsIncludingTypedWord) {
- final SuggestedWords suggestedWords;
- if (suggestedWordsIncludingTypedWord.size() > 1) {
- // 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 #getSuggestedWordsExcludingTypedWord() method sets
- // willAutoCorrect to false.
- suggestedWords = suggestedWordsIncludingTypedWord
- .getSuggestedWordsExcludingTypedWord();
- } 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;
- }
- // We need to pass typedWord because mWordComposer.mTypedWord may
- // differ from typedWord.
- unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
- suggestedWords, typedWord);
- }});
- } else {
- // We found suggestion spans in the word. We'll create the SuggestedWords out of
- // them, and make willAutoCorrect false.
- final SuggestedWords suggestedWords = new SuggestedWords(suggestions,
- true /* typedWordValid */, false /* willAutoCorrect */,
- false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
- false /* isPrediction */);
- // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord.
- unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords, typedWord);
- }
- }
-
public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
final SuggestedWords suggestedWords, final String typedWord) {
// Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
@@ -2955,127 +1578,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching
// the text to adapt it.
// TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition)
- mIsAutoCorrectionIndicatorOn = false;
+ mInputLogic.mIsAutoCorrectionIndicatorOn = false;
mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord);
}
- /**
- * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
- * word, else do nothing.
- */
- private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
- final CharSequence word =
- mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent());
- if (null != word) {
- final String wordString = word.toString();
- restartSuggestionsOnWordBeforeCursor(wordString);
- // TODO: Handle the case where the user manually moves the cursor and then backs up over
- // a separator. In that case, the current log unit should not be uncommitted.
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString,
- true /* dumpCurrentLogUnit */);
- }
- }
- }
-
- private void restartSuggestionsOnWordBeforeCursor(final String word) {
- mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
- final int length = word.length();
- mConnection.deleteSurroundingText(length, 0);
- mConnection.setComposingText(word, 1);
- mHandler.postUpdateSuggestionStrip();
- }
-
- /**
- * Retry resetting caches in the rich input connection.
- *
- * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
- * This method handles the retry, and re-schedules a new retry if we still can't access.
- * We only retry up to 5 times before giving up.
- *
- * @param tryResumeSuggestions Whether we should resume suggestions or not.
- * @param remainingTries How many times we may try again before giving up.
- */
- private void retryResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
- if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(mLastSelectionStart, false)) {
- if (0 < remainingTries) {
- mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
- return;
- }
- // If remainingTries is 0, we should stop waiting for new tries, but it's still
- // better to load the keyboard (less things will be broken).
- }
- tryFixLyingCursorPosition();
- mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
- if (tryResumeSuggestions) mHandler.postResumeSuggestions();
- }
-
- private void revertCommit() {
- final String previousWord = mLastComposedWord.mPrevWord;
- final String originallyTypedWord = mLastComposedWord.mTypedWord;
- final String committedWord = mLastComposedWord.mCommittedWord;
- final int cancelLength = committedWord.length();
- // We want java chars, not codepoints for the following.
- final int separatorLength = mLastComposedWord.mSeparatorString.length();
- // TODO: should we check our saved separator against the actual contents of the text view?
- final int deleteLength = cancelLength + separatorLength;
- if (DEBUG) {
- if (mWordComposer.isComposingWord()) {
- throw new RuntimeException("revertCommit, but we are composing a word");
- }
- final CharSequence wordBeforeCursor =
- mConnection.getTextBeforeCursor(deleteLength, 0)
- .subSequence(0, cancelLength);
- if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
- throw new RuntimeException("revertCommit check failed: we thought we were "
- + "reverting \"" + committedWord
- + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
- }
- }
- mConnection.deleteSurroundingText(deleteLength, 0);
- if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
- mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
- }
- final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
- if (mSettings.getCurrent().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.
- mConnection.commitText(stringToCommit, 1);
- } else {
- // For languages without spaces, we revert the typed string but the cursor is flush
- // with the typed word, so we need to resume suggestions right away.
- mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard());
- mConnection.setComposingText(stringToCommit, 1);
- }
- if (mSettings.isInternal()) {
- LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
- Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
- }
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord,
- mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString);
- }
- // Don't restart suggestion yet. We'll restart if the user deletes the
- // separator.
- mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
- // We have a separator between the word and the cursor: we should show predictions.
- mHandler.postUpdateSuggestionStrip();
- }
-
- // This essentially inserts a space, and that's it.
- public void promotePhantomSpace() {
- final SettingsValues currentSettings = mSettings.getCurrent();
- if (currentSettings.shouldInsertSpacesAutomatically()
- && currentSettings.mCurrentLanguageHasSpaces
- && !mConnection.textBeforeCursorLooksLikeURL()) {
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_promotePhantomSpace();
- }
- sendKeyCodePoint(Constants.CODE_SPACE);
- }
- }
-
// TODO: Make this private
// Outside LatinIME, only used by the {@link InputTestsBase} test suite.
@UsedForTesting
@@ -3095,12 +1601,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private void hapticAndAudioFeedback(final int code, final int repeatCount) {
final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView();
- if (keyboardView != null && keyboardView.isInSlidingKeyInput()) {
- // No need to feedback while sliding input.
+ if (keyboardView != null && keyboardView.isInDraggingFinger()) {
+ // No need to feedback while finger is dragging.
return;
}
if (repeatCount > 0) {
- if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) {
+ if (code == Constants.CODE_DELETE && !mInputLogic.mConnection.canDeleteCharacters()) {
// No need to feedback when repeat delete key will have no effect.
return;
}
@@ -3154,9 +1660,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if
// it doesn't know what to do with it and leave it to the application. For example,
// hardware key events for adjusting the screen's brightness are passed as is.
- if (mEventInterpreter.onHardwareKeyEvent(event)) {
+ if (mInputLogic.mEventInterpreter.onHardwareKeyEvent(event)) {
final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
- mCurrentlyPressedHardwareKeys.add(keyIdentifier);
+ mInputLogic.mCurrentlyPressedHardwareKeys.add(keyIdentifier);
return true;
}
return super.onKeyDown(keyCode, event);
@@ -3165,7 +1671,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public boolean onKeyUp(final int keyCode, final KeyEvent event) {
final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
- if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
+ if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
return true;
}
return super.onKeyUp(keyCode, event);
@@ -3190,14 +1696,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
};
private void launchSettings() {
- handleClose();
+ mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR);
+ requestHideSelf(0);
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView != null) {
+ mainKeyboardView.closing();
+ }
launchSubActivity(SettingsActivity.class);
}
public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) {
// Put the text in the attached EditText into a safe, saved state before switching to a
// new activity that will also use the soft keyboard.
- commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR);
launchSubActivity(activityClass);
}
@@ -3215,7 +1726,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final CharSequence[] items = new CharSequence[] {
// TODO: Should use new string "Select active input modes".
getString(R.string.language_selection_title),
- getString(ApplicationUtils.getAcitivityTitleResId(this, SettingsActivity.class)),
+ getString(ApplicationUtils.getActivityTitleResId(this, SettingsActivity.class)),
};
final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
@Override
@@ -3226,8 +1737,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
mRichImm.getInputMethodIdOfThisIme(),
Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
- | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
break;
case 1:
@@ -3236,9 +1747,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
};
- final AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setItems(items, listener)
- .setTitle(title);
+ final AlertDialog.Builder builder =
+ new AlertDialog.Builder(this).setItems(items, listener).setTitle(title);
showOptionDialog(builder.create());
}
@@ -3264,31 +1774,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// TODO: can this be removed somehow without breaking the tests?
@UsedForTesting
- /* package for test */ String getFirstSuggestedWord() {
- return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null;
+ /* package for test */ SuggestedWords getSuggestedWords() {
+ // You may not use this method for anything else than debug
+ return DEBUG ? mInputLogic.mSuggestedWords : null;
}
// DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
@UsedForTesting
/* package for test */ boolean isCurrentlyWaitingForMainDictionary() {
- return mSuggest.isCurrentlyWaitingForMainDictionary();
- }
-
- // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
- @UsedForTesting
- /* package for test */ boolean hasMainDictionary() {
- return mSuggest.hasMainDictionary();
+ return mInputLogic.mSuggest.mDictionaryFacilitator.isCurrentlyWaitingForMainDictionary();
}
// DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
@UsedForTesting
/* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
- mSuggest.resetMainDict(this, locale, null);
+ mInputLogic.mSuggest.mDictionaryFacilitator.reloadMainDict(this, locale, null);
}
public void debugDumpStateAndCrashWithException(final String context) {
- final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString());
- s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes)
+ final SettingsValues settingsValues = mSettings.getCurrent();
+ final StringBuilder s = new StringBuilder(settingsValues.toString());
+ s.append("\nAttributes : ").append(settingsValues.mInputAttributes)
.append("\nContext : ").append(context);
throw new RuntimeException(s.toString());
}
@@ -3299,17 +1805,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final Printer p = new PrintWriterPrinter(fout);
p.println("LatinIME state :");
+ p.println(" VersionCode = " + ApplicationUtils.getVersionCode(this));
+ p.println(" VersionName = " + ApplicationUtils.getVersionName(this));
final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
p.println(" Keyboard mode = " + keyboardMode);
final SettingsValues settingsValues = mSettings.getCurrent();
- p.println(" mIsSuggestionsSuggestionsRequested = "
- + settingsValues.isSuggestionsRequested(mDisplayOrientation));
+ p.println(" mIsSuggestionsRequested = " + settingsValues.isSuggestionsRequested());
p.println(" mCorrectionEnabled=" + settingsValues.mCorrectionEnabled);
- p.println(" isComposingWord=" + mWordComposer.isComposingWord());
+ p.println(" isComposingWord=" + mInputLogic.mWordComposer.isComposingWord());
p.println(" mSoundOn=" + settingsValues.mSoundOn);
p.println(" mVibrateOn=" + settingsValues.mVibrateOn);
p.println(" mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn);
p.println(" inputAttributes=" + settingsValues.mInputAttributes);
+ // TODO: Dump all settings values
}
}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 673d1b4c2..4d174ddb8 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -57,14 +57,19 @@ public final class RichInputConnection {
private static final int INVALID_CURSOR_POSITION = -1;
/**
- * This variable contains an expected value for the cursor position. This is where the
- * cursor may end up after all the keyboard-triggered updates have passed. We keep this to
- * compare it to the actual cursor position to guess whether the move was caused by a
- * keyboard command or not.
- * It's not really the cursor position: the cursor may not be there yet, and it's also expected
- * there be cases where it never actually comes to be there.
+ * This variable contains an expected value for the selection start position. This is where the
+ * cursor or selection start may end up after all the keyboard-triggered updates have passed. We
+ * keep this to compare it to the actual selection start to guess whether the move was caused by
+ * a keyboard command or not.
+ * It's not really the selection start position: the selection start may not be there yet, and
+ * in some cases, it may never arrive there.
*/
- private int mExpectedCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points
+ private int mExpectedSelStart = INVALID_CURSOR_POSITION; // in chars, not code points
+ /**
+ * The expected selection end. Only differs from mExpectedSelStart if a non-empty selection is
+ * expected. The same caveats as mExpectedSelStart apply.
+ */
+ private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points
/**
* This contains the committed text immediately preceding the cursor and the composing
* text if any. It is refreshed when the cursor moves by calling upon the TextView.
@@ -103,16 +108,16 @@ public final class RichInputConnection {
final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString()
: beforeCursor.subSequence(beforeCursor.length() - actualLength,
beforeCursor.length()).toString();
- if (et.selectionStart != mExpectedCursorPosition
+ if (et.selectionStart != mExpectedSelStart
|| !(reference.equals(internal.toString()))) {
- final String context = "Expected cursor position = " + mExpectedCursorPosition
- + "\nActual cursor position = " + et.selectionStart
+ final String context = "Expected selection start = " + mExpectedSelStart
+ + "\nActual selection start = " + et.selectionStart
+ "\nExpected text = " + internal.length() + " " + internal
+ "\nActual text = " + reference.length() + " " + reference;
((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
} else {
Log.e(TAG, DebugLogUtils.getStackTrace(2));
- Log.e(TAG, "Exp <> Actual : " + mExpectedCursorPosition + " <> " + et.selectionStart);
+ Log.e(TAG, "Exp <> Actual : " + mExpectedSelStart + " <> " + et.selectionStart);
}
}
@@ -150,16 +155,50 @@ public final class RichInputConnection {
* data, so we empty the cache and note that we don't know the new cursor position, and we
* return false so that the caller knows about this and can retry later.
*
- * @param newCursorPosition The new position of the cursor, as received from the system.
- * @param shouldFinishComposition Whether we should finish the composition in progress.
+ * @param newSelStart the new position of the selection start, as received from the system.
+ * @param newSelEnd the new position of the selection end, as received from the system.
+ * @param shouldFinishComposition whether we should finish the composition in progress.
* @return true if we were able to connect to the editor successfully, false otherwise. When
* this method returns false, the caches could not be correctly refreshed so they were only
* reset: the caller should try again later to return to normal operation.
*/
- public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newCursorPosition,
- final boolean shouldFinishComposition) {
- mExpectedCursorPosition = newCursorPosition;
+ public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newSelStart,
+ final int newSelEnd, final boolean shouldFinishComposition) {
+ mExpectedSelStart = newSelStart;
+ mExpectedSelEnd = newSelEnd;
mComposingText.setLength(0);
+ final boolean didReloadTextSuccessfully = reloadTextCache();
+ if (!didReloadTextSuccessfully) {
+ Log.d(TAG, "Will try to retrieve text later.");
+ return false;
+ }
+ final int lengthOfTextBeforeCursor = mCommittedTextBeforeComposingText.length();
+ if (lengthOfTextBeforeCursor > newSelStart
+ || (lengthOfTextBeforeCursor < Constants.EDITOR_CONTENTS_CACHE_SIZE
+ && newSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
+ // newSelStart and newSelEnd may be lying -- when rotating the device (probably a
+ // framework bug). If we have less chars than we asked for, then we know how many chars
+ // we have, and if we got more than newSelStart says, then we know it was lying. In both
+ // cases the length is more reliable. Note that we only have to check newSelStart (not
+ // newSelEnd) since if newSelEnd is wrong, the newSelStart will be wrong as well.
+ mExpectedSelStart = lengthOfTextBeforeCursor;
+ mExpectedSelEnd = lengthOfTextBeforeCursor;
+ }
+ if (null != mIC && shouldFinishComposition) {
+ mIC.finishComposingText();
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.richInputConnection_finishComposingText();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Reload the cached text from the InputConnection.
+ *
+ * @return true if successful
+ */
+ private boolean reloadTextCache() {
mCommittedTextBeforeComposingText.setLength(0);
mIC = mParent.getCurrentInputConnection();
// Call upon the inputconnection directly since our own method is using the cache, and
@@ -169,27 +208,12 @@ public final class RichInputConnection {
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.
- mExpectedCursorPosition = INVALID_CURSOR_POSITION;
- Log.e(TAG, "Unable to connect to the editor to retrieve text... will retry later");
+ mExpectedSelStart = INVALID_CURSOR_POSITION;
+ mExpectedSelEnd = INVALID_CURSOR_POSITION;
+ Log.e(TAG, "Unable to connect to the editor to retrieve text.");
return false;
}
mCommittedTextBeforeComposingText.append(textBeforeCursor);
- final int lengthOfTextBeforeCursor = textBeforeCursor.length();
- if (lengthOfTextBeforeCursor > newCursorPosition
- || (lengthOfTextBeforeCursor < Constants.EDITOR_CONTENTS_CACHE_SIZE
- && newCursorPosition < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
- // newCursorPosition may be lying -- when rotating the device (probably a framework
- // bug). If we have less chars than we asked for, then we know how many chars we have,
- // and if we got more than newCursorPosition says, then we know it was lying. In both
- // cases the length is more reliable
- mExpectedCursorPosition = lengthOfTextBeforeCursor;
- }
- if (null != mIC && shouldFinishComposition) {
- mIC.finishComposingText();
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.richInputConnection_finishComposingText();
- }
- }
return true;
}
@@ -218,7 +242,8 @@ public final class RichInputConnection {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
mCommittedTextBeforeComposingText.append(text);
- mExpectedCursorPosition += text.length() - mComposingText.length();
+ mExpectedSelStart += text.length() - mComposingText.length();
+ mExpectedSelEnd = mExpectedSelStart;
mComposingText.setLength(0);
if (null != mIC) {
mIC.commitText(text, i);
@@ -231,7 +256,7 @@ public final class RichInputConnection {
}
public boolean canDeleteCharacters() {
- return mExpectedCursorPosition > 0;
+ return mExpectedSelStart > 0;
}
/**
@@ -268,11 +293,10 @@ public final class RichInputConnection {
// heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so.
// getCapsMode should be updated to be able to return a "not enough info" result so that
// we can get more context only when needed.
- if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedCursorPosition) {
- final CharSequence textBeforeCursor = getTextBeforeCursor(
- Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
- if (!TextUtils.isEmpty(textBeforeCursor)) {
- mCommittedTextBeforeComposingText.append(textBeforeCursor);
+ if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedSelStart) {
+ if (!reloadTextCache()) {
+ Log.w(TAG, "Unable to connect to the editor. "
+ + "Setting caps mode without knowing text.");
}
}
// This never calls InputConnection#getCapsMode - in fact, it's a static method that
@@ -295,8 +319,8 @@ public final class RichInputConnection {
// However, if we don't have an expected cursor position, then we should always
// go fetch the cache again (as it happens, INVALID_CURSOR_POSITION < 0, so we need to
// test for this explicitly)
- if (INVALID_CURSOR_POSITION != mExpectedCursorPosition
- && (cachedLength >= n || cachedLength >= mExpectedCursorPosition)) {
+ if (INVALID_CURSOR_POSITION != mExpectedSelStart
+ && (cachedLength >= n || cachedLength >= mExpectedSelStart)) {
final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText);
// We call #toString() here to create a temporary object.
// In some situations, this method is called on a worker thread, and it's possible
@@ -336,10 +360,14 @@ public final class RichInputConnection {
+ remainingChars, 0);
mCommittedTextBeforeComposingText.setLength(len);
}
- if (mExpectedCursorPosition > beforeLength) {
- mExpectedCursorPosition -= beforeLength;
+ if (mExpectedSelStart > beforeLength) {
+ mExpectedSelStart -= beforeLength;
+ mExpectedSelEnd -= beforeLength;
} else {
- mExpectedCursorPosition = 0;
+ // There are fewer characters before the cursor in the buffer than we are being asked to
+ // delete. Only delete what is there.
+ mExpectedSelStart = 0;
+ mExpectedSelEnd -= mExpectedSelStart;
}
if (null != mIC) {
mIC.deleteSurroundingText(beforeLength, afterLength);
@@ -373,7 +401,8 @@ public final class RichInputConnection {
switch (keyEvent.getKeyCode()) {
case KeyEvent.KEYCODE_ENTER:
mCommittedTextBeforeComposingText.append("\n");
- mExpectedCursorPosition += 1;
+ mExpectedSelStart += 1;
+ mExpectedSelEnd = mExpectedSelStart;
break;
case KeyEvent.KEYCODE_DEL:
if (0 == mComposingText.length()) {
@@ -385,18 +414,24 @@ public final class RichInputConnection {
} else {
mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
}
- if (mExpectedCursorPosition > 0) mExpectedCursorPosition -= 1;
+ if (mExpectedSelStart > 0 && mExpectedSelStart == mExpectedSelEnd) {
+ // TODO: Handle surrogate pairs.
+ mExpectedSelStart -= 1;
+ }
+ mExpectedSelEnd = mExpectedSelStart;
break;
case KeyEvent.KEYCODE_UNKNOWN:
if (null != keyEvent.getCharacters()) {
mCommittedTextBeforeComposingText.append(keyEvent.getCharacters());
- mExpectedCursorPosition += keyEvent.getCharacters().length();
+ mExpectedSelStart += keyEvent.getCharacters().length();
+ mExpectedSelEnd = mExpectedSelStart;
}
break;
default:
- final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1);
+ final String text = StringUtils.newSingleCodePointString(keyEvent.getUnicodeChar());
mCommittedTextBeforeComposingText.append(text);
- mExpectedCursorPosition += text.length();
+ mExpectedSelStart += text.length();
+ mExpectedSelEnd = mExpectedSelStart;
break;
}
}
@@ -430,10 +465,12 @@ public final class RichInputConnection {
public void setComposingText(final CharSequence text, final int newCursorPosition) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
- mExpectedCursorPosition += text.length() - mComposingText.length();
+ mExpectedSelStart += text.length() - mComposingText.length();
+ mExpectedSelEnd = mExpectedSelStart;
mComposingText.setLength(0);
mComposingText.append(text);
- // TODO: support values of i != 1. At this time, this is never called with i != 1.
+ // TODO: support values of newCursorPosition != 1. At this time, this is never called with
+ // newCursorPosition != 1.
if (null != mIC) {
mIC.setComposingText(text, newCursorPosition);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -443,19 +480,31 @@ public final class RichInputConnection {
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
}
- public void setSelection(final int start, final int end) {
+ /**
+ * Set the selection of the text editor.
+ *
+ * Calls through to {@link InputConnection#setSelection(int, int)}.
+ *
+ * @param start the character index where the selection should start.
+ * @param end the character index where the selection should end.
+ * @return Returns true on success, false if the input connection is no longer valid either when
+ * setting the selection or when retrieving the text cache at that point.
+ */
+ public boolean setSelection(final int start, final int end) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ mExpectedSelStart = start;
+ mExpectedSelEnd = end;
if (null != mIC) {
- mIC.setSelection(start, end);
+ final boolean isIcValid = mIC.setSelection(start, end);
+ if (!isIcValid) {
+ return false;
+ }
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.richInputConnection_setSelection(start, end);
}
}
- mExpectedCursorPosition = start;
- mCommittedTextBeforeComposingText.setLength(0);
- mCommittedTextBeforeComposingText.append(
- getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0));
+ return reloadTextCache();
}
public void commitCorrection(final CorrectionInfo correctionInfo) {
@@ -476,7 +525,8 @@ public final class RichInputConnection {
// text should never be null, but just in case, it's better to insert nothing than to crash
if (null == text) text = "";
mCommittedTextBeforeComposingText.append(text);
- mExpectedCursorPosition += text.length() - mComposingText.length();
+ mExpectedSelStart += text.length() - mComposingText.length();
+ mExpectedSelEnd = mExpectedSelStart;
mComposingText.setLength(0);
if (null != mIC) {
mIC.commitCompletion(completionInfo);
@@ -488,7 +538,7 @@ public final class RichInputConnection {
}
@SuppressWarnings("unused")
- public String getNthPreviousWord(final String sentenceSeperators, final int n) {
+ public String getNthPreviousWord(final SettingsValues currentSettingsValues, final int n) {
mIC = mParent.getCurrentInputConnection();
if (null == mIC) return null;
final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
@@ -507,7 +557,7 @@ public final class RichInputConnection {
}
}
}
- return getNthPreviousWord(prev, sentenceSeperators, n);
+ return getNthPreviousWord(prev, currentSettingsValues, n);
}
private static boolean isSeparator(int code, String sep) {
@@ -531,7 +581,7 @@ public final class RichInputConnection {
// (n = 2) "abc |" -> null
// (n = 2) "abc. def|" -> null
public static String getNthPreviousWord(final CharSequence prev,
- final String sentenceSeperators, final int n) {
+ final SettingsValues currentSettingsValues, final int n) {
if (prev == null) return null;
final String[] w = spaceRegex.split(prev);
@@ -543,7 +593,8 @@ public final class RichInputConnection {
// If ends in a separator, return null
final char lastChar = nthPrevWord.charAt(length - 1);
- if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+ if (currentSettingsValues.isWordSeparator(lastChar)
+ || currentSettingsValues.isWordConnector(lastChar)) return null;
return nthPrevWord;
}
@@ -655,45 +706,6 @@ public final class RichInputConnection {
return TextUtils.equals(text, beforeText);
}
- /* (non-javadoc)
- * Returns the word before the cursor if the cursor is at the end of a word, null otherwise
- */
- public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) {
- // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
- // separator or end of line/text)
- // Example: "test|"<EOL> "te|st" get rejected here
- final CharSequence textAfterCursor = getTextAfterCursor(1, 0);
- if (!TextUtils.isEmpty(textAfterCursor)
- && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null;
-
- // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
- // Example: " -|" gets rejected here but "e-|" and "e|" are okay
- CharSequence word = getWordAtCursor(settings.mWordSeparators);
- // We don't suggest on leading single quotes, so we have to remove them from the word if
- // it starts with single quotes.
- while (!TextUtils.isEmpty(word) && Constants.CODE_SINGLE_QUOTE == word.charAt(0)) {
- word = word.subSequence(1, word.length());
- }
- if (TextUtils.isEmpty(word)) return null;
- // Find the last code point of the string
- final int lastCodePoint = Character.codePointBefore(word, word.length());
- // If for some reason the text field contains non-unicode binary data, or if the
- // charsequence is exactly one char long and the contents is a low surrogate, return null.
- if (!Character.isDefined(lastCodePoint)) return null;
- // Bail out if the cursor is not at the end of a word (cursor must be preceded by
- // non-whitespace, non-separator, non-start-of-text)
- // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
- if (settings.isWordSeparator(lastCodePoint)) return null;
- final char firstChar = word.charAt(0); // we just tested that word is not empty
- if (word.length() == 1 && !Character.isLetter(firstChar)) return null;
-
- // We don't restart suggestion if the first character is not a letter, because we don't
- // start composing when the first character is not a letter.
- if (!Character.isLetter(firstChar)) return null;
-
- return word;
- }
-
public boolean revertDoubleSpacePeriod() {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
// Here we test whether we indeed have a period and a space before us. This should not
@@ -758,20 +770,30 @@ public final class RichInputConnection {
* this update and not the ones in-between. This is almost impossible to achieve even trying
* very very hard.
*
- * @param oldSelStart The value of the old cursor position in the update.
- * @param newSelStart The value of the new cursor position in the update.
+ * @param oldSelStart The value of the old selection in the update.
+ * @param newSelStart The value of the new selection in the update.
+ * @param oldSelEnd The value of the old selection end in the update.
+ * @param newSelEnd The value of the new selection end in the update.
* @return whether this is a belated expected update or not.
*/
- public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart) {
- // If this is an update that arrives at our expected position, it's a belated update.
- if (newSelStart == mExpectedCursorPosition) return true;
- // If this is an update that moves the cursor from our expected position, it must be
- // an explicit move.
- if (oldSelStart == mExpectedCursorPosition) return false;
- // The following returns true if newSelStart is between oldSelStart and
- // mCurrentCursorPosition. We assume that if the updated position is between the old
- // position and the expected position, then it must be a belated update.
- return (newSelStart - oldSelStart) * (mExpectedCursorPosition - newSelStart) >= 0;
+ public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart,
+ final int oldSelEnd, final int newSelEnd) {
+ // This update is "belated" if we are expecting it. That is, mExpectedSelStart and
+ // mExpectedSelEnd match the new values that the TextView is updating TO.
+ if (mExpectedSelStart == newSelStart && mExpectedSelEnd == newSelEnd) return true;
+ // This update is not belated if mExpectedSelStart and mExpeectedSelend match the old
+ // values, and one of newSelStart or newSelEnd is updated to a different value. In this
+ // case, there is likely something other than the IME that has moved the selection endpoint
+ // to the new value.
+ if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd
+ && (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false;
+ // If nether of the above two cases holds, then the system may be having trouble keeping up
+ // with updates. If 1) the selection is a cursor, 2) newSelStart is between oldSelStart
+ // and mExpectedSelStart, and 3) newSelEnd is between oldSelEnd and mExpectedSelEnd, then
+ // assume a belated update.
+ return (newSelStart == newSelEnd)
+ && (newSelStart - oldSelStart) * (mExpectedSelStart - newSelStart) >= 0
+ && (newSelEnd - oldSelEnd) * (mExpectedSelEnd - newSelEnd) >= 0;
}
/**
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index cd9c89f04..860575a1f 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -32,7 +32,9 @@ 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.latin.utils.LocaleUtils;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
import java.util.List;
@@ -56,23 +58,34 @@ public final class SubtypeSwitcher {
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 InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE = new InputMethodSubtype(
- R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark,
- SubtypeLocaleUtils.NO_LANGUAGE, "keyboard", "KeyboardLayoutSet="
- + SubtypeLocaleUtils.QWERTY
- + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
- + ",EnabledWhenDefaultIsNotAsciiCapable,"
- + Constants.Subtype.ExtraValue.EMOJI_CAPABLE,
- false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */);
+ 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 InputMethodSubtype DUMMY_EMOJI_SUBTYPE = new InputMethodSubtype(
- R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark,
- SubtypeLocaleUtils.NO_LANGUAGE, "keyboard", "KeyboardLayoutSet="
- + SubtypeLocaleUtils.EMOJI + ","
- + Constants.Subtype.ExtraValue.EMOJI_CAPABLE,
- false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */);
+ 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);
static final class NeedsToDisplayLanguage {
private int mEnabledSubtypeCount;
@@ -213,6 +226,7 @@ public final class SubtypeSwitcher {
}
public boolean isShortcutImeEnabled() {
+ updateShortcutIME();
if (mShortcutInputMethodInfo == null) {
return false;
}
@@ -224,10 +238,13 @@ public final class SubtypeSwitcher {
}
public boolean isShortcutImeReady() {
- if (mShortcutInputMethodInfo == null)
+ updateShortcutIME();
+ if (mShortcutInputMethodInfo == null) {
return false;
- if (mShortcutSubtype == null)
+ }
+ if (mShortcutSubtype == null) {
return true;
+ }
if (mShortcutSubtype.containsExtraValueKey(REQ_NETWORK_CONNECTIVITY)) {
return mIsNetworkConnected;
}
@@ -256,18 +273,35 @@ public final class SubtypeSwitcher {
return mNeedsToDisplayLanguage.getValue();
}
- private static Locale sForcedLocaleForTesting = null;
+ public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypes() {
+ final Locale systemLocale = mResources.getConfiguration().locale;
+ final List<InputMethodSubtype> enabledSubtypesOfThisIme =
+ mRichImm.getMyEnabledInputMethodSubtypeList(true);
+ for (final InputMethodSubtype subtype : enabledSubtypesOfThisIme) {
+ if (!systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static InputMethodSubtype sForcedSubtypeForTesting = null;
@UsedForTesting
- void forceLocale(final Locale locale) {
- sForcedLocaleForTesting = locale;
+ void forceSubtype(final InputMethodSubtype subtype) {
+ sForcedSubtypeForTesting = subtype;
}
public Locale getCurrentSubtypeLocale() {
- if (null != sForcedLocaleForTesting) return sForcedLocaleForTesting;
+ if (null != sForcedSubtypeForTesting) {
+ return LocaleUtils.constructLocaleFromString(sForcedSubtypeForTesting.getLocale());
+ }
return SubtypeLocaleUtils.getSubtypeLocale(getCurrentSubtype());
}
public InputMethodSubtype getCurrentSubtype() {
+ if (null != sForcedSubtypeForTesting) {
+ return sForcedSubtypeForTesting;
+ }
return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype());
}
@@ -279,8 +313,8 @@ public final class SubtypeSwitcher {
if (mNoLanguageSubtype != null) {
return mNoLanguageSubtype;
}
- Log.w(TAG, "Can't find no lanugage with QWERTY subtype");
- Log.w(TAG, "No input method subtype found; return dummy subtype: "
+ 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;
}
@@ -293,8 +327,9 @@ public final class SubtypeSwitcher {
if (mEmojiSubtype != null) {
return mEmojiSubtype;
}
- Log.w(TAG, "Can't find Emoji subtype");
- Log.w(TAG, "No input method subtype found; return dummy subtype: " + DUMMY_EMOJI_SUBTYPE);
+ 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/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 0a4c7a55d..fc85c1388 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -16,18 +16,10 @@
package com.android.inputmethod.latin;
-import android.content.Context;
-import android.preference.PreferenceManager;
import android.text.TextUtils;
-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.personalization.PersonalizationDictionary;
-import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary;
-import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
-import com.android.inputmethod.latin.settings.Settings;
import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
import com.android.inputmethod.latin.utils.BoundedTreeSet;
import com.android.inputmethod.latin.utils.CollectionUtils;
@@ -35,9 +27,7 @@ import com.android.inputmethod.latin.utils.StringUtils;
import java.util.ArrayList;
import java.util.Comparator;
-import java.util.HashSet;
import java.util.Locale;
-import java.util.concurrent.ConcurrentHashMap;
/**
* This class loads a dictionary and provides a list of suggestions for a given sequence of
@@ -62,148 +52,27 @@ public final class Suggest {
public static final int MAX_SUGGESTIONS = 18;
- public interface SuggestInitializationListener {
- public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable);
- }
-
private static final boolean DBG = LatinImeLogger.sDBG;
- private final ConcurrentHashMap<String, Dictionary> mDictionaries =
- CollectionUtils.newConcurrentHashMap();
- private HashSet<String> mOnlyDictionarySetForDebug = null;
- private Dictionary mMainDictionary;
- private ContactsBinaryDictionary mContactsDict;
- @UsedForTesting
- private boolean mIsCurrentlyWaitingForMainDictionary = false;
+ public final DictionaryFacilitatorForSuggest mDictionaryFacilitator;
private float mAutoCorrectionThreshold;
// Locale used for upper- and title-casing words
public final Locale mLocale;
- public Suggest(final Context context, final Locale locale,
- final SuggestInitializationListener listener) {
- initAsynchronously(context, locale, listener);
- mLocale = locale;
- // initialize a debug flag for the personalization
- if (Settings.readUseOnlyPersonalizationDictionaryForDebug(
- PreferenceManager.getDefaultSharedPreferences(context))) {
- mOnlyDictionarySetForDebug = new HashSet<String>();
- mOnlyDictionarySetForDebug.add(Dictionary.TYPE_PERSONALIZATION);
- mOnlyDictionarySetForDebug.add(Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA);
- }
- }
-
- @UsedForTesting
- Suggest(final AssetFileAddress[] dictionaryList, final Locale locale) {
- final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(dictionaryList,
- false /* useFullEditDistance */, locale);
+ public Suggest(final Locale locale,
+ final DictionaryFacilitatorForSuggest dictionaryFacilitator) {
mLocale = locale;
- mMainDictionary = mainDict;
- addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, mainDict);
- }
-
- private void initAsynchronously(final Context context, final Locale locale,
- final SuggestInitializationListener listener) {
- resetMainDict(context, locale, listener);
- }
-
- private void addOrReplaceDictionaryInternal(final String key, final Dictionary dict) {
- if (mOnlyDictionarySetForDebug != null && !mOnlyDictionarySetForDebug.contains(key)) {
- Log.w(TAG, "Ignore add " + key + " dictionary for debug.");
- return;
- }
- addOrReplaceDictionary(mDictionaries, key, dict);
- }
-
- private static void addOrReplaceDictionary(
- final ConcurrentHashMap<String, Dictionary> dictionaries,
- final String key, final Dictionary dict) {
- final Dictionary oldDict = (dict == null)
- ? dictionaries.remove(key)
- : dictionaries.put(key, dict);
- if (oldDict != null && dict != oldDict) {
- oldDict.close();
- }
- }
-
- public void resetMainDict(final Context context, final Locale locale,
- final SuggestInitializationListener listener) {
- mIsCurrentlyWaitingForMainDictionary = true;
- mMainDictionary = null;
- if (listener != null) {
- listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
- }
- new Thread("InitializeBinaryDictionary") {
- @Override
- public void run() {
- final DictionaryCollection newMainDict =
- DictionaryFactory.createMainDictionaryFromManager(context, locale);
- addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, newMainDict);
- mMainDictionary = newMainDict;
- if (listener != null) {
- listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
- }
- mIsCurrentlyWaitingForMainDictionary = false;
- }
- }.start();
- }
-
- // The main dictionary could have been loaded asynchronously. Don't cache the return value
- // of this method.
- public boolean hasMainDictionary() {
- return null != mMainDictionary && mMainDictionary.isInitialized();
- }
-
- @UsedForTesting
- public boolean isCurrentlyWaitingForMainDictionary() {
- return mIsCurrentlyWaitingForMainDictionary;
+ mDictionaryFacilitator = dictionaryFacilitator;
}
- public Dictionary getMainDictionary() {
- return mMainDictionary;
- }
-
- public ContactsBinaryDictionary getContactsDictionary() {
- return mContactsDict;
- }
-
- public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() {
- return mDictionaries;
- }
-
- /**
- * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
- * before the main dictionary, if set. This refers to the system-managed user dictionary.
- */
- public void setUserDictionary(final UserBinaryDictionary userDictionary) {
- addOrReplaceDictionaryInternal(Dictionary.TYPE_USER, userDictionary);
- }
-
- /**
- * Sets an optional contacts dictionary resource to be loaded. It is also possible to remove
- * the contacts dictionary by passing null to this method. In this case no contacts dictionary
- * won't be used.
- */
- public void setContactsDictionary(final ContactsBinaryDictionary contactsDictionary) {
- mContactsDict = contactsDictionary;
- addOrReplaceDictionaryInternal(Dictionary.TYPE_CONTACTS, contactsDictionary);
- }
-
- public void setUserHistoryDictionary(final UserHistoryDictionary userHistoryDictionary) {
- addOrReplaceDictionaryInternal(Dictionary.TYPE_USER_HISTORY, userHistoryDictionary);
- }
-
- public void setPersonalizationPredictionDictionary(
- final PersonalizationPredictionDictionary personalizationPredictionDictionary) {
- addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA,
- personalizationPredictionDictionary);
- }
-
- public void setPersonalizationDictionary(
- final PersonalizationDictionary personalizationDictionary) {
- addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION,
- personalizationDictionary);
+ // Creates instance with new dictionary facilitator.
+ public Suggest(final Suggest oldSuggst,
+ final DictionaryFacilitatorForSuggest dictionaryFacilitator) {
+ mLocale = oldSuggst.mLocale;
+ mAutoCorrectionThreshold = oldSuggst.mAutoCorrectionThreshold;
+ mDictionaryFacilitator = dictionaryFacilitator;
}
public void setAutoCorrectionThreshold(float threshold) {
@@ -257,14 +126,9 @@ public final class Suggest {
} else {
wordComposerForLookup = wordComposer;
}
-
- for (final String key : mDictionaries.keySet()) {
- final Dictionary dictionary = mDictionaries.get(key);
- suggestionsSet.addAll(dictionary.getSuggestions(wordComposerForLookup,
- prevWordForBigram, proximityInfo, blockOffensiveWords,
- additionalFeaturesOptions));
- }
-
+ mDictionaryFacilitator.getSuggestions(wordComposerForLookup, prevWordForBigram,
+ proximityInfo, blockOffensiveWords, additionalFeaturesOptions, SESSION_TYPING,
+ suggestionsSet);
final String whitelistedWord;
if (suggestionsSet.isEmpty()) {
whitelistedWord = null;
@@ -278,7 +142,7 @@ public final class Suggest {
// or if it's a 2+ characters non-word (i.e. it's not in the dictionary).
final boolean allowsToBeAutoCorrected = (null != whitelistedWord
&& !whitelistedWord.equals(consideredWord))
- || (consideredWord.length() > 1 && !AutoCorrectionUtils.isValidWord(this,
+ || (consideredWord.length() > 1 && !mDictionaryFacilitator.isValidWord(
consideredWord, wordComposer.isFirstCharCapitalized()));
final boolean hasAutoCorrection;
@@ -289,7 +153,8 @@ public final class Suggest {
// the word *would* have been auto-corrected.
if (!isCorrectionEnabled || !allowsToBeAutoCorrected || !wordComposer.isComposingWord()
|| suggestionsSet.isEmpty() || wordComposer.hasDigits()
- || wordComposer.isMostlyCaps() || wordComposer.isResumed() || !hasMainDictionary()
+ || wordComposer.isMostlyCaps() || wordComposer.isResumed()
+ || !mDictionaryFacilitator.hasMainDictionary()
|| SuggestedWordInfo.KIND_SHORTCUT == suggestionsSet.first().mKind) {
// 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
@@ -362,15 +227,8 @@ public final class Suggest {
final OnGetSuggestedWordsCallback callback) {
final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
MAX_SUGGESTIONS);
-
- // At second character typed, search the unigrams (scores being affected by bigrams)
- for (final String key : mDictionaries.keySet()) {
- final Dictionary dictionary = mDictionaries.get(key);
- suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId(wordComposer,
- prevWordForBigram, proximityInfo, blockOffensiveWords,
- additionalFeaturesOptions, sessionId));
- }
-
+ mDictionaryFacilitator.getSuggestions(wordComposer, prevWordForBigram, proximityInfo,
+ blockOffensiveWords, additionalFeaturesOptions, sessionId, suggestionsSet);
for (SuggestedWordInfo wordInfo : suggestionsSet) {
LatinImeLogger.onAddSuggestedWord(wordInfo.mWord, wordInfo.mSourceDict.mDictType);
}
@@ -432,7 +290,8 @@ public final class Suggest {
final String scoreInfoString;
if (normalizedScore > 0) {
scoreInfoString = String.format(
- Locale.ROOT, "%d (%4.2f)", cur.mScore, normalizedScore);
+ Locale.ROOT, "%d (%4.2f), %s", cur.mScore, normalizedScore,
+ cur.mSourceDict.mDictType);
} else {
scoreInfoString = Integer.toString(cur.mScore);
}
@@ -483,11 +342,6 @@ public final class Suggest {
}
public void close() {
- final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet();
- dictionaries.addAll(mDictionaries.values());
- for (final Dictionary dictionary : dictionaries) {
- dictionary.close();
- }
- mMainDictionary = null;
+ mDictionaryFacilitator.close();
}
}
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 97c89dd4e..f9de89c80 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -104,10 +104,6 @@ public final class SuggestedWords {
return debugString;
}
- public boolean willAutoCorrect() {
- return mWillAutoCorrect;
- }
-
@Override
public String toString() {
// Pretty-print method to help debug
@@ -150,7 +146,7 @@ public final class SuggestedWords {
for (int index = 1; index < previousSize; index++) {
final SuggestedWordInfo prevWordInfo = previousSuggestions.getInfo(index);
final String prevWord = prevWordInfo.mWord;
- // Filter out duplicate suggestion.
+ // Filter out duplicate suggestions.
if (!alreadySeen.contains(prevWord)) {
suggestionsList.add(prevWordInfo);
alreadySeen.add(prevWord);
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
index 3213c92c7..9c095e4b1 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
@@ -25,34 +25,26 @@ import java.util.ArrayList;
import java.util.Locale;
public final class SynchronouslyLoadedContactsBinaryDictionary extends ContactsBinaryDictionary {
- private boolean mClosed;
+ private final Object mLock = new Object();
public SynchronouslyLoadedContactsBinaryDictionary(final Context context, final Locale locale) {
super(context, locale);
}
@Override
- public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
final String prevWordForBigrams, final ProximityInfo proximityInfo,
final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
- reloadDictionaryIfRequired();
- return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords,
- additionalFeaturesOptions);
+ synchronized (mLock) {
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo,
+ blockOffensiveWords, additionalFeaturesOptions);
+ }
}
@Override
- public synchronized boolean isValidWord(final String word) {
- reloadDictionaryIfRequired();
- return isValidWordInner(word);
- }
-
- // Protect against multiple closing
- @Override
- public synchronized void close() {
- // Actually with the current implementation of ContactsDictionary it's safe to close
- // several times, so the following protection is really only for foolproofing
- if (mClosed) return;
- mClosed = true;
- super.close();
+ public boolean isValidWord(final String word) {
+ synchronized (mLock) {
+ return super.isValidWord(word);
+ }
}
}
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
index 6405b5e46..9ccd9e4e8 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
@@ -22,30 +22,34 @@ import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import java.util.ArrayList;
+import java.util.Locale;
public final class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionary {
+ private final Object mLock = new Object();
- public SynchronouslyLoadedUserBinaryDictionary(final Context context, final String locale) {
+ public SynchronouslyLoadedUserBinaryDictionary(final Context context, final Locale locale) {
this(context, locale, false);
}
- public SynchronouslyLoadedUserBinaryDictionary(final Context context, final String locale,
+ public SynchronouslyLoadedUserBinaryDictionary(final Context context, final Locale locale,
final boolean alsoUseMoreRestrictiveLocales) {
super(context, locale, alsoUseMoreRestrictiveLocales);
}
@Override
- public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
final String prevWordForBigrams, final ProximityInfo proximityInfo,
final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
- reloadDictionaryIfRequired();
- return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords,
- additionalFeaturesOptions);
+ synchronized (mLock) {
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo,
+ blockOffensiveWords, additionalFeaturesOptions);
+ }
}
@Override
- public synchronized boolean isValidWord(final String word) {
- reloadDictionaryIfRequired();
- return isValidWordInner(word);
+ public boolean isValidWord(final String word) {
+ synchronized (mLock) {
+ return super.isValidWord(word);
+ }
}
}
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 15b3d8d02..8011247c6 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -18,7 +18,6 @@ package com.android.inputmethod.latin;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
-import android.content.ContentUris;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
@@ -29,10 +28,12 @@ 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.latin.utils.LocaleUtils;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
+import java.io.File;
import java.util.Arrays;
import java.util.Locale;
@@ -74,21 +75,29 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
private ContentObserver mObserver;
final private String mLocale;
final private boolean mAlsoUseMoreRestrictiveLocales;
+ final public boolean mEnabled;
- public UserBinaryDictionary(final Context context, final String locale) {
+ public UserBinaryDictionary(final Context context, final Locale locale) {
this(context, locale, false);
}
- public UserBinaryDictionary(final Context context, final String locale,
+ // Dummy constructor for tests.
+ @UsedForTesting
+ public UserBinaryDictionary(final Context context, final Locale locale, final File file) {
+ this(context, locale);
+ }
+
+ public UserBinaryDictionary(final Context context, final Locale locale,
final boolean alsoUseMoreRestrictiveLocales) {
- super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER,
+ super(context, getDictNameWithLocale(NAME, locale), locale, Dictionary.TYPE_USER,
false /* isUpdatable */);
if (null == locale) throw new NullPointerException(); // Catch the error earlier
- if (SubtypeLocaleUtils.NO_LANGUAGE.equals(locale)) {
+ 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;
} else {
- mLocale = locale;
+ mLocale = localeStr;
}
mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
// Perform a managed query. The Activity will handle closing and re-querying the cursor
@@ -112,7 +121,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
}
};
cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
-
+ mEnabled = readIsEnabled();
loadDictionary();
}
@@ -190,7 +199,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
}
}
- public boolean isEnabled() {
+ private boolean readIsEnabled() {
final ContentResolver cr = mContext.getContentResolver();
final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI);
if (client != null) {
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 039dadc66..f078c7346 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -48,6 +48,11 @@ public final class WordComposer {
// at any given time. However this is not limited in size, while mPrimaryKeyCodes is limited
// to MAX_WORD_LENGTH code points.
private final StringBuilder mTypedWord;
+ // The previous word (before the composing word). Used as context for suggestions. May be null
+ // after resetting and before starting a new composing word, or when there is no context like
+ // at the start of text for example. It can also be set to null externally when the user
+ // enters a separator that does not let bigrams across, like a period or a comma.
+ private String mPreviousWordForSuggestion;
private String mAutoCorrection;
private boolean mIsResumed;
private boolean mIsBatchMode;
@@ -85,6 +90,7 @@ public final class WordComposer {
mIsBatchMode = false;
mCursorPositionWithinWord = 0;
mRejectedBatchModeSuggestion = null;
+ mPreviousWordForSuggestion = null;
refreshSize();
}
@@ -101,6 +107,7 @@ public final class WordComposer {
mIsBatchMode = source.mIsBatchMode;
mCursorPositionWithinWord = source.mCursorPositionWithinWord;
mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion;
+ mPreviousWordForSuggestion = source.mPreviousWordForSuggestion;
refreshSize();
}
@@ -118,6 +125,7 @@ public final class WordComposer {
mIsBatchMode = false;
mCursorPositionWithinWord = 0;
mRejectedBatchModeSuggestion = null;
+ mPreviousWordForSuggestion = null;
refreshSize();
}
@@ -284,8 +292,13 @@ public final class WordComposer {
/**
* Set the currently composing word to the one passed as an argument.
* This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
+ * @param word the char sequence to set as the composing word.
+ * @param previousWord the previous word, to use as context for suggestions. Can be null if
+ * the context is nil (typically, at start of text).
+ * @param keyboard the keyboard this is typed on, for coordinate info/proximity.
*/
- public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
+ public void setComposingWord(final CharSequence word, final String previousWord,
+ final Keyboard keyboard) {
reset();
final int length = word.length();
for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
@@ -293,6 +306,7 @@ public final class WordComposer {
addKeyInfo(codePoint, keyboard);
}
mIsResumed = true;
+ mPreviousWordForSuggestion = previousWord;
}
/**
@@ -343,6 +357,10 @@ public final class WordComposer {
return mTypedWord.toString();
}
+ public String getPreviousWordForSuggestion() {
+ return mPreviousWordForSuggestion;
+ }
+
/**
* Whether or not the user typed a capital letter as the first letter in the word
* @return capitalization preference
@@ -388,18 +406,21 @@ public final class WordComposer {
}
/**
- * Saves the caps mode at the start of composing.
+ * Saves the caps mode and the previous word at the start of composing.
*
- * WordComposer needs to know about this for several reasons. The first is, we need to know
- * after the fact what the reason was, to register the correct form into the user history
- * dictionary: if the word was automatically capitalized, we should insert it in all-lower
- * case but if it's a manual pressing of shift, then it should be inserted as is.
+ * WordComposer needs to know about the caps mode for several reasons. The first is, we need
+ * to know after the fact what the reason was, to register the correct form into the user
+ * history dictionary: if the word was automatically capitalized, we should insert it in
+ * all-lower case but if it's a manual pressing of shift, then it should be inserted as is.
* Also, batch input needs to know about the current caps mode to display correctly
* capitalized suggestions.
* @param mode the mode at the time of start
+ * @param previousWord the previous word as context for suggestions. May be null if none.
*/
- public void setCapitalizedModeAtStartComposingTime(final int mode) {
+ public void setCapitalizedModeAndPreviousWordAtStartComposingTime(final int mode,
+ final String previousWord) {
mCapitalizedMode = mode;
+ mPreviousWordForSuggestion = previousWord;
}
/**
@@ -451,6 +472,7 @@ public final class WordComposer {
mCapsCount = 0;
mDigitsCount = 0;
mIsBatchMode = false;
+ mPreviousWordForSuggestion = committedWord;
mTypedWord.setLength(0);
mCodePointSize = 0;
mTrailingSingleQuotesCount = 0;
@@ -464,7 +486,15 @@ public final class WordComposer {
return lastComposedWord;
}
- public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
+ // Call this when the recorded previous word should be discarded. This is typically called
+ // when the user inputs a separator that's not whitespace (including the case of the
+ // double-space-to-period feature).
+ public void discardPreviousWordForSuggestion() {
+ mPreviousWordForSuggestion = null;
+ }
+
+ public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord,
+ final String previousWord) {
mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
mInputPointers.set(lastComposedWord.mInputPointers);
mTypedWord.setLength(0);
@@ -475,6 +505,7 @@ public final class WordComposer {
mCursorPositionWithinWord = mCodePointSize;
mRejectedBatchModeSuggestion = null;
mIsResumed = true;
+ mPreviousWordForSuggestion = previousWord;
}
public boolean isBatchMode() {
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
new file mode 100644
index 000000000..c867ab3d3
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -0,0 +1,1744 @@
+/*
+ * 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.inputlogic;
+
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.text.style.SuggestionSpan;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.EditorInfo;
+
+import com.android.inputmethod.compat.SuggestionSpanUtils;
+import com.android.inputmethod.event.EventInterpreter;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.keyboard.MainKeyboardView;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.InputPointers;
+import com.android.inputmethod.latin.LastComposedWord;
+import com.android.inputmethod.latin.LatinIME;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.RichInputConnection;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+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.define.ProductionFlag;
+import com.android.inputmethod.latin.settings.Settings;
+import com.android.inputmethod.latin.settings.SettingsValues;
+import com.android.inputmethod.latin.suggestions.SuggestionStripView;
+import com.android.inputmethod.latin.utils.AsyncResultHolder;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.InputTypeUtils;
+import com.android.inputmethod.latin.utils.LatinImeLoggerUtils;
+import com.android.inputmethod.latin.utils.RecapitalizeStatus;
+import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.utils.TextRange;
+import com.android.inputmethod.research.ResearchLogger;
+
+import java.util.ArrayList;
+import java.util.TreeSet;
+
+/**
+ * This class manages the input logic.
+ */
+public final class InputLogic {
+ private static final String TAG = InputLogic.class.getSimpleName();
+
+ // TODO : Remove this member when we can.
+ private final LatinIME mLatinIME;
+
+ private InputLogicHandler mInputLogicHandler;
+
+ // TODO : make all these fields private as soon as possible.
+ // Current space state of the input method. This can be any of the above constants.
+ public int mSpaceState;
+ // Never null
+ public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
+ public Suggest mSuggest;
+ // The event interpreter should never be null.
+ public EventInterpreter mEventInterpreter;
+
+ public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+ public final WordComposer mWordComposer;
+ public final RichInputConnection mConnection;
+ public final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
+
+ // Keep track of the last selection range to decide if we need to show word alternatives
+ public int mLastSelectionStart = Constants.NOT_A_CURSOR_POSITION;
+ public int mLastSelectionEnd = Constants.NOT_A_CURSOR_POSITION;
+
+ private int mDeleteCount;
+ private long mLastKeyTime;
+ public final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
+
+ // Keeps track of most recently inserted text (multi-character key) for reverting
+ private String mEnteredText;
+
+ // TODO: This boolean is persistent state and causes large side effects at unexpected times.
+ // Find a way to remove it for readability.
+ public boolean mIsAutoCorrectionIndicatorOn;
+
+ public InputLogic(final LatinIME latinIME) {
+ mLatinIME = latinIME;
+ mWordComposer = new WordComposer();
+ mEventInterpreter = new EventInterpreter(latinIME);
+ mConnection = new RichInputConnection(latinIME);
+ mInputLogicHandler = null;
+ }
+
+ /**
+ * Initializes the input logic for input in an editor.
+ *
+ * Call this when input starts or restarts in some editor (typically, in onStartInputView).
+ * If the input is starting in the same field as before, set `restarting' to true. This allows
+ * the input logic to reset only necessary stuff and save performance. Also, when restarting
+ * some things must not be done (for example, the keyboard should not be reset to the
+ * alphabetic layout), so do not send false to this just in case.
+ *
+ * @param restarting whether input is starting in the same field as before. Unused for now.
+ * @param editorInfo the editorInfo associated with the editor.
+ */
+ public void startInput(final boolean restarting, final EditorInfo editorInfo) {
+ mEnteredText = null;
+ resetComposingState(true /* alsoResetLastComposedWord */);
+ mDeleteCount = 0;
+ mSpaceState = SpaceState.NONE;
+ mRecapitalizeStatus.deactivate();
+ mCurrentlyPressedHardwareKeys.clear();
+ mSuggestedWords = SuggestedWords.EMPTY;
+ mLastSelectionStart = editorInfo.initialSelStart;
+ mLastSelectionEnd = editorInfo.initialSelEnd;
+ // 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.
+ tryFixLyingCursorPosition();
+ mInputLogicHandler = new InputLogicHandler(mLatinIME, this);
+ }
+
+ /**
+ * Clean up the input logic after input is finished.
+ */
+ public void finishInput() {
+ if (mWordComposer.isComposingWord()) {
+ mConnection.finishComposingText();
+ }
+ resetComposingState(true /* alsoResetLastComposedWord */);
+ mInputLogicHandler.destroy();
+ mInputLogicHandler = null;
+ }
+
+ /**
+ * React to a string input.
+ *
+ * This is triggered by keys that input many characters at once, like the ".com" key or
+ * some additional keys for example.
+ *
+ * @param settingsValues the current values of the settings.
+ * @param rawText the text to input.
+ */
+ public void onTextInput(final SettingsValues settingsValues, final String rawText,
+ // TODO: remove this argument
+ final LatinIME.UIHandler handler) {
+ mConnection.beginBatchEdit();
+ if (mWordComposer.isComposingWord()) {
+ commitCurrentAutoCorrection(settingsValues, rawText, handler);
+ } else {
+ resetComposingState(true /* alsoResetLastComposedWord */);
+ }
+ handler.postUpdateSuggestionStrip();
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS
+ && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) {
+ ResearchLogger.getInstance().onResearchKeySelected(mLatinIME);
+ return;
+ }
+ final String text = performSpecificTldProcessingOnTextInput(rawText);
+ if (SpaceState.PHANTOM == mSpaceState) {
+ promotePhantomSpace(settingsValues);
+ }
+ mConnection.commitText(text, 1);
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */);
+ }
+ mConnection.endBatchEdit();
+ // Space state must be updated before calling updateShiftState
+ mSpaceState = SpaceState.NONE;
+ mEnteredText = text;
+ }
+
+ /**
+ * React to a code input. It may be a code point to insert, or a symbolic value that influences
+ * the keyboard behavior.
+ *
+ * Typically, this is called whenever a key is pressed on the software keyboard. This is not
+ * the entry point for gesture input; see the onBatchInput* family of functions for this.
+ *
+ * @param code the code to handle. It may be a code point, or an internal key code.
+ * @param x the x-coordinate where the user pressed the key, or NOT_A_COORDINATE.
+ * @param y the y-coordinate where the user pressed the key, or NOT_A_COORDINATE.
+ */
+ public void onCodeInput(final int code, final int x, final int y,
+ // TODO: remove these three arguments
+ final LatinIME.UIHandler handler,
+ final KeyboardSwitcher keyboardSwitcher, final SubtypeSwitcher subtypeSwitcher) {
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_onCodeInput(code, x, y);
+ }
+ final SettingsValues settingsValues = Settings.getInstance().getCurrent();
+ final long when = SystemClock.uptimeMillis();
+ if (code != Constants.CODE_DELETE
+ || when > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) {
+ mDeleteCount = 0;
+ }
+ mLastKeyTime = when;
+ mConnection.beginBatchEdit();
+ // The space state depends only on the last character pressed and its own previous
+ // state. Here, we revert the space state to neutral if the key is actually modifying
+ // the input contents (any non-shift key), which is what we should do for
+ // all inputs that do not result in a special state. Each character handling is then
+ // free to override the state as they see fit.
+ final int spaceState = mSpaceState;
+ if (!mWordComposer.isComposingWord()) {
+ mIsAutoCorrectionIndicatorOn = false;
+ }
+
+ // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
+ if (code != Constants.CODE_SPACE) {
+ handler.cancelDoubleSpacePeriodTimer();
+ }
+
+ boolean didAutoCorrect = false;
+ switch (code) {
+ case Constants.CODE_DELETE:
+ handleBackspace(settingsValues, spaceState, handler, keyboardSwitcher);
+ LatinImeLogger.logOnDelete(x, y);
+ break;
+ case Constants.CODE_SHIFT:
+ // Note: Calling back to the keyboard on Shift key is handled in
+ // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
+ final Keyboard currentKeyboard = keyboardSwitcher.getKeyboard();
+ if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
+ // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
+ // alphabetic shift and shift while in symbol layout.
+ performRecapitalization(settingsValues);
+ keyboardSwitcher.updateShiftState();
+ }
+ break;
+ case Constants.CODE_CAPSLOCK:
+ // Note: Changing keyboard to shift lock state is handled in
+ // {@link KeyboardSwitcher#onCodeInput(int)}.
+ break;
+ case Constants.CODE_SWITCH_ALPHA_SYMBOL:
+ // Note: Calling back to the keyboard on symbol key is handled in
+ // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
+ break;
+ case Constants.CODE_SETTINGS:
+ onSettingsKeyPressed();
+ break;
+ case Constants.CODE_SHORTCUT:
+ subtypeSwitcher.switchToShortcutIME(mLatinIME);
+ break;
+ case Constants.CODE_ACTION_NEXT:
+ performEditorAction(EditorInfo.IME_ACTION_NEXT);
+ break;
+ case Constants.CODE_ACTION_PREVIOUS:
+ performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
+ break;
+ case Constants.CODE_LANGUAGE_SWITCH:
+ handleLanguageSwitchKey();
+ break;
+ case Constants.CODE_EMOJI:
+ // Note: Switching emoji keyboard is being handled in
+ // {@link KeyboardState#onCodeInput(int,int)}.
+ break;
+ case Constants.CODE_ENTER:
+ final EditorInfo editorInfo = getCurrentInputEditorInfo();
+ final int imeOptionsActionId =
+ InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
+ if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
+ // Either we have an actionLabel and we should performEditorAction with actionId
+ // regardless of its value.
+ performEditorAction(editorInfo.actionId);
+ } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
+ // We didn't have an actionLabel, but we had another action to execute.
+ // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
+ // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
+ // means there should be an action and the app didn't bother to set a specific
+ // code for it - presumably it only handles one. It does not have to be treated
+ // in any specific way: anything that is not IME_ACTION_NONE should be sent to
+ // performEditorAction.
+ performEditorAction(imeOptionsActionId);
+ } else {
+ // No action label, and the action from imeOptions is NONE: this is a regular
+ // enter key that should input a carriage return.
+ didAutoCorrect = handleNonSpecialCharacter(settingsValues, Constants.CODE_ENTER,
+ x, y, spaceState, keyboardSwitcher, handler);
+ }
+ break;
+ case Constants.CODE_SHIFT_ENTER:
+ didAutoCorrect = handleNonSpecialCharacter(settingsValues, Constants.CODE_ENTER,
+ x, y, spaceState, keyboardSwitcher, handler);
+ break;
+ default:
+ didAutoCorrect = handleNonSpecialCharacter(settingsValues,
+ code, x, y, spaceState, keyboardSwitcher, handler);
+ break;
+ }
+ keyboardSwitcher.onCodeInput(code);
+ // Reset after any single keystroke, except shift, capslock, and symbol-shift
+ if (!didAutoCorrect && code != Constants.CODE_SHIFT
+ && code != Constants.CODE_CAPSLOCK
+ && code != Constants.CODE_SWITCH_ALPHA_SYMBOL)
+ mLastComposedWord.deactivate();
+ if (Constants.CODE_DELETE != code) {
+ mEnteredText = null;
+ }
+ mConnection.endBatchEdit();
+ }
+
+ 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 */);
+ handler.cancelUpdateSuggestionStrip();
+ mConnection.beginBatchEdit();
+ if (mWordComposer.isComposingWord()) {
+ if (settingsValues.mIsInternal) {
+ if (mWordComposer.isBatchMode()) {
+ LatinImeLoggerUtils.onAutoCorrection("", mWordComposer.getTypedWord(), " ",
+ mWordComposer);
+ }
+ }
+ final int wordComposerSize = mWordComposer.size();
+ // Since isComposingWord() is true, the size is at least 1.
+ 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.
+ resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+ } else if (wordComposerSize <= 1) {
+ // We auto-correct the previous (typed, not gestured) string iff it's one character
+ // long. The reason for this is, even in the middle of gesture typing, you'll still
+ // tap one-letter words and you want them auto-corrected (typically, "i" in English
+ // should become "I"). However for any longer word, we assume that the reason for
+ // tapping probably is that the word you intend to type is not in the dictionary,
+ // so we do not attempt to correct, on the assumption that if that was a dictionary
+ // word, the user would probably have gestured instead.
+ commitCurrentAutoCorrection(settingsValues, LastComposedWord.NOT_A_SEPARATOR,
+ handler);
+ } else {
+ commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR);
+ }
+ }
+ final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
+ if (Character.isLetterOrDigit(codePointBeforeCursor)
+ || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
+ final boolean autoShiftHasBeenOverriden = keyboardSwitcher.getKeyboardShiftMode() !=
+ getCurrentAutoCapsState(settingsValues);
+ mSpaceState = SpaceState.PHANTOM;
+ if (!autoShiftHasBeenOverriden) {
+ // When we change the space state, we need to update the shift state of the
+ // keyboard unless it has been overridden manually. This is happening for example
+ // after typing some letters and a period, then gesturing; the keyboard is not in
+ // caps mode yet, but since a gesture is starting, it should go in caps mode,
+ // unless the user explictly said it should not.
+ keyboardSwitcher.updateShiftState();
+ }
+ }
+ mConnection.endBatchEdit();
+ mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
+ getActualCapsMode(settingsValues, keyboardSwitcher.getKeyboardShiftMode()),
+ // Prev word is 1st word before cursor
+ getNthPreviousWordForSuggestion(settingsValues, 1 /* nthPreviousWord */));
+ }
+
+ /* The sequence number member is only used in onUpdateBatchInput. It is increased each time
+ * auto-commit happens. The reason we need this is, when auto-commit happens we trim the
+ * input pointers that are held in a singleton, and to know how much to trim we rely on the
+ * results of the suggestion process that is held in mSuggestedWords.
+ * However, the suggestion process is asynchronous, and sometimes we may enter the
+ * onUpdateBatchInput method twice without having recomputed suggestions yet, or having
+ * received new suggestions generated from not-yet-trimmed input pointers. In this case, the
+ * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we
+ * remove an unrelated number of pointers (possibly even more than are left in the input
+ * pointers, leading to a crash).
+ * To avoid that, we increase the sequence number each time we auto-commit and trim the
+ * input pointers, and we do not use any suggested words that have been generated with an
+ * 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(" ", 2);
+ batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
+ promotePhantomSpace(settingsValues);
+ mConnection.commitText(commitParts[0], 0);
+ mSpaceState = SpaceState.PHANTOM;
+ keyboardSwitcher.updateShiftState();
+ mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
+ getActualCapsMode(settingsValues,
+ keyboardSwitcher.getKeyboardShiftMode()), commitParts[0]);
+ ++mAutoCommitSequenceNumber;
+ }
+ }
+ }
+ mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
+ }
+
+ public void onEndBatchInput(final SettingsValues settingValues,
+ final InputPointers batchPointers) {
+ mInputLogicHandler.onEndBatchInput(batchPointers, mAutoCommitSequenceNumber);
+ }
+
+ // TODO: remove this argument
+ public void onCancelBatchInput(final LatinIME.UIHandler handler) {
+ mInputLogicHandler.onCancelBatchInput();
+ handler.showGesturePreviewAndSuggestionStrip(
+ SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
+ }
+
+ /**
+ * Handle inputting a code point to the editor.
+ *
+ * Non-special keys are those that generate a single code point.
+ * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
+ * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
+ * any key that results in multiple code points like the ".com" key.
+ *
+ * @param settingsValues The current settings values.
+ * @param codePoint the code point associated with the key.
+ * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+ * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+ * @param spaceState the space state at start of the batch input.
+ * @return whether this caused an auto-correction to happen.
+ */
+ private boolean handleNonSpecialCharacter(final SettingsValues settingsValues,
+ final int codePoint, final int x, final int y, final int spaceState,
+ // TODO: remove these arguments
+ final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+ mSpaceState = SpaceState.NONE;
+ final boolean didAutoCorrect;
+ if (settingsValues.isWordSeparator(codePoint)
+ || Character.getType(codePoint) == Character.OTHER_SYMBOL) {
+ didAutoCorrect = handleSeparator(settingsValues, codePoint, x, y, spaceState,
+ keyboardSwitcher, handler);
+ } else {
+ didAutoCorrect = false;
+ if (SpaceState.PHANTOM == spaceState) {
+ if (settingsValues.mIsInternal) {
+ if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
+ LatinImeLoggerUtils.onAutoCorrection("", mWordComposer.getTypedWord(), " ",
+ mWordComposer);
+ }
+ }
+ 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 character at the current cursor position.
+ resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+ } else {
+ commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR);
+ }
+ }
+ final int keyX, keyY;
+ final Keyboard keyboard = keyboardSwitcher.getKeyboard();
+ if (keyboard != null && keyboard.hasProximityCharsCorrection(codePoint)) {
+ keyX = x;
+ keyY = y;
+ } else {
+ keyX = Constants.NOT_A_COORDINATE;
+ keyY = Constants.NOT_A_COORDINATE;
+ }
+ handleNonSeparator(settingsValues, codePoint, keyX, keyY, spaceState,
+ keyboardSwitcher, handler);
+ }
+ return didAutoCorrect;
+ }
+
+ /**
+ * Handle a non-separator.
+ * @param settingsValues The current settings values.
+ * @param codePoint the code point associated with the key.
+ * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+ * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+ * @param spaceState the space state at start of the batch input.
+ */
+ private void handleNonSeparator(final SettingsValues settingsValues,
+ final int codePoint, final int x, final int y, final int spaceState,
+ // TODO: Remove these arguments
+ final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+ // TODO: refactor this method to stop flipping isComposingWord around all the time, and
+ // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
+ // which has the same name as other handle* methods but is not the same.
+ boolean isComposingWord = mWordComposer.isComposingWord();
+
+ // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
+ // See onStartBatchInput() to see how to do it.
+ if (SpaceState.PHANTOM == spaceState && !settingsValues.isWordConnector(codePoint)) {
+ if (isComposingWord) {
+ // Sanity check
+ throw new RuntimeException("Should not be composing here");
+ }
+ promotePhantomSpace(settingsValues);
+ }
+
+ 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 character at the current cursor position.
+ resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+ isComposingWord = false;
+ }
+ // We want to find out whether to start composing a new word with this character. If so,
+ // we need to reset the composing state and switch isComposingWord. The order of the
+ // tests is important for good performance.
+ // We only start composing if we're not already composing.
+ if (!isComposingWord
+ // We only start composing if this is a word code point. Essentially that means it's a
+ // a letter or a word connector.
+ && settingsValues.isWordCodePoint(codePoint)
+ // We never go into composing state if suggestions are not requested.
+ && settingsValues.isSuggestionsRequested() &&
+ // In languages with spaces, we only start composing a word when we are not already
+ // touching a word. In languages without spaces, the above conditions are sufficient.
+ (!mConnection.isCursorTouchingWord(settingsValues)
+ || !settingsValues.mCurrentLanguageHasSpaces)) {
+ // Reset entirely the composing state anyway, then start composing a new word unless
+ // the character is a single quote or a dash. The idea here is, single quote and dash
+ // are not separators and they should be treated as normal characters, except in the
+ // first position where they should not start composing a word.
+ isComposingWord = (Constants.CODE_SINGLE_QUOTE != codePoint
+ && Constants.CODE_DASH != codePoint);
+ // Here we don't need to reset the last composed word. It will be reset
+ // when we commit this one, if we ever do; if on the other hand we backspace
+ // it entirely and resume suggestions on the previous word, we'd like to still
+ // have touch coordinates for it.
+ resetComposingState(false /* alsoResetLastComposedWord */);
+ }
+ if (isComposingWord) {
+ final MainKeyboardView mainKeyboardView = keyboardSwitcher.getMainKeyboardView();
+ // TODO: We should reconsider which coordinate system should be used to represent
+ // keyboard event.
+ final int keyX = mainKeyboardView.getKeyX(x);
+ final int keyY = mainKeyboardView.getKeyY(y);
+ mWordComposer.add(codePoint, keyX, keyY);
+ // If it's the first letter, make note of auto-caps state
+ if (mWordComposer.size() == 1) {
+ // We pass 1 to getPreviousWordForSuggestion because we were not composing a word
+ // yet, so the word we want is the 1st word before the cursor.
+ mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
+ getActualCapsMode(settingsValues, keyboardSwitcher.getKeyboardShiftMode()),
+ getNthPreviousWordForSuggestion(settingsValues, 1 /* nthPreviousWord */));
+ }
+ mConnection.setComposingText(getTextWithUnderline(
+ mWordComposer.getTypedWord()), 1);
+ } else {
+ final boolean swapWeakSpace = maybeStripSpace(settingsValues,
+ codePoint, spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
+
+ sendKeyCodePoint(settingsValues, codePoint);
+
+ if (swapWeakSpace) {
+ swapSwapperAndSpace(keyboardSwitcher);
+ mSpaceState = SpaceState.WEAK;
+ }
+ // In case the "add to dictionary" hint was still displayed.
+ mLatinIME.dismissAddToDictionaryHint();
+ }
+ handler.postUpdateSuggestionStrip();
+ if (settingsValues.mIsInternal) {
+ LatinImeLoggerUtils.onNonSeparator((char)codePoint, x, y);
+ }
+ }
+
+ /**
+ * Handle input of a separator code point.
+ * @param settingsValues The current settings values.
+ * @param codePoint the code point associated with the key.
+ * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+ * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+ * @param spaceState the space state at start of the batch input.
+ * @return whether this caused an auto-correction to happen.
+ */
+ private boolean handleSeparator(final SettingsValues settingsValues,
+ final int codePoint, final int x, final int y, final int spaceState,
+ // TODO: remove these arguments
+ final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+ boolean didAutoCorrect = false;
+ // We avoid sending spaces in languages without spaces if we were composing.
+ final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint
+ && !settingsValues.mCurrentLanguageHasSpaces
+ && 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 separator at the current cursor position.
+ resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+ }
+ // isComposingWord() may have changed since we stored wasComposing
+ if (mWordComposer.isComposingWord()) {
+ if (settingsValues.mCorrectionEnabled) {
+ final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
+ : StringUtils.newSingleCodePointString(codePoint);
+ commitCurrentAutoCorrection(settingsValues, separator, handler);
+ didAutoCorrect = true;
+ } else {
+ commitTyped(settingsValues, StringUtils.newSingleCodePointString(codePoint));
+ }
+ }
+
+ final boolean swapWeakSpace = maybeStripSpace(settingsValues, codePoint, spaceState,
+ Constants.SUGGESTION_STRIP_COORDINATE == x);
+
+ if (SpaceState.PHANTOM == spaceState &&
+ settingsValues.isUsuallyPrecededBySpace(codePoint)) {
+ promotePhantomSpace(settingsValues);
+ }
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_handleSeparator(codePoint, mWordComposer.isComposingWord());
+ }
+
+ if (!shouldAvoidSendingCode) {
+ sendKeyCodePoint(settingsValues, codePoint);
+ }
+
+ if (Constants.CODE_SPACE == codePoint) {
+ if (settingsValues.isSuggestionsRequested()) {
+ if (maybeDoubleSpacePeriod(settingsValues, handler)) {
+ keyboardSwitcher.updateShiftState();
+ mSpaceState = SpaceState.DOUBLE;
+ } else if (!mLatinIME.isShowingPunctuationList()) {
+ mSpaceState = SpaceState.WEAK;
+ }
+ }
+
+ handler.startDoubleSpacePeriodTimer();
+ handler.postUpdateSuggestionStrip();
+ } else {
+ if (swapWeakSpace) {
+ swapSwapperAndSpace(keyboardSwitcher);
+ mSpaceState = SpaceState.SWAP_PUNCTUATION;
+ } else if (SpaceState.PHANTOM == spaceState
+ && settingsValues.isUsuallyFollowedBySpace(codePoint)) {
+ // If we are in phantom space state, and the user presses a separator, we want to
+ // stay in phantom space state so that the next keypress has a chance to add the
+ // space. For example, if I type "Good dat", pick "day" from the suggestion strip
+ // then insert a comma and go on to typing the next word, I want the space to be
+ // inserted automatically before the next word, the same way it is when I don't
+ // input the comma.
+ // The case is a little different if the separator is a space stripper. Such a
+ // separator does not normally need a space on the right (that's the difference
+ // between swappers and strippers), so we should not stay in phantom space state if
+ // the separator is a stripper. Hence the additional test above.
+ mSpaceState = SpaceState.PHANTOM;
+ }
+
+ // Set punctuation right away. onUpdateSelection will fire but tests whether it is
+ // already displayed or not, so it's okay.
+ mLatinIME.setPunctuationSuggestions();
+ }
+ if (settingsValues.mIsInternal) {
+ LatinImeLoggerUtils.onSeparator((char)codePoint, x, y);
+ }
+
+ keyboardSwitcher.updateShiftState();
+ return didAutoCorrect;
+ }
+
+ /**
+ * Handle a press on the backspace key.
+ * @param settingsValues The current settings values.
+ * @param spaceState The space state at start of this batch edit.
+ */
+ private void handleBackspace(final SettingsValues settingsValues, final int spaceState,
+ // TODO: remove these arguments
+ final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher) {
+ mSpaceState = SpaceState.NONE;
+ final int deleteCountAtStart = mDeleteCount;
+ mDeleteCount++;
+
+ // In many cases, we may have to put the keyboard in auto-shift state again. However
+ // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
+ // during key repeat.
+ handler.postUpdateShiftState();
+
+ if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+ // If we are in the middle of a recorrection, we need to commit the recorrection
+ // first so that we can remove the character at the current cursor position.
+ resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+ // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
+ }
+ if (mWordComposer.isComposingWord()) {
+ if (mWordComposer.isBatchMode()) {
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ final String word = mWordComposer.getTypedWord();
+ ResearchLogger.latinIME_handleBackspace_batch(word, 1);
+ }
+ final String rejectedSuggestion = mWordComposer.getTypedWord();
+ mWordComposer.reset();
+ mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
+ } else {
+ mWordComposer.deleteLast();
+ }
+ mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+ handler.postUpdateSuggestionStrip();
+ if (!mWordComposer.isComposingWord()) {
+ // If we just removed the last character, auto-caps mode may have changed so we
+ // need to re-evaluate.
+ keyboardSwitcher.updateShiftState();
+ }
+ } else {
+ if (mLastComposedWord.canRevertCommit()) {
+ if (settingsValues.mIsInternal) {
+ LatinImeLoggerUtils.onAutoCorrectionCancellation();
+ }
+ revertCommit(settingsValues, keyboardSwitcher, handler);
+ return;
+ }
+ if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
+ // Cancel multi-character input: remove the text we just entered.
+ // This is triggered on backspace after a key that inputs multiple characters,
+ // like the smiley key or the .com key.
+ mConnection.deleteSurroundingText(mEnteredText.length(), 0);
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
+ }
+ 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
+ // reverting any autocorrect at this point. So we can safely return.
+ return;
+ }
+ if (SpaceState.DOUBLE == spaceState) {
+ handler.cancelDoubleSpacePeriodTimer();
+ if (mConnection.revertDoubleSpacePeriod()) {
+ // No need to reset mSpaceState, it has already be done (that's why we
+ // receive it as a parameter)
+ return;
+ }
+ } else if (SpaceState.SWAP_PUNCTUATION == spaceState) {
+ if (mConnection.revertSwapPunctuation()) {
+ // Likewise
+ return;
+ }
+ }
+
+ // 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 (mLastSelectionStart != mLastSelectionEnd) {
+ // If there is a selection, remove it.
+ final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
+ mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+ // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
+ // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
+ // but we want to set it right away to avoid it being used with the wrong values
+ // later (typically, in a subsequent press on backspace).
+ mLastSelectionEnd = mLastSelectionStart;
+ mConnection.deleteSurroundingText(numCharsDeleted, 0);
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_handleBackspace(numCharsDeleted,
+ false /* shouldUncommitLogUnit */);
+ }
+ } else {
+ // There is no selection, just delete one character.
+ if (Constants.NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
+ // This should never happen.
+ Log.e(TAG, "Backspace when we don't know the selection position");
+ }
+ if (settingsValues.isBeforeJellyBean() ||
+ settingsValues.mInputAttributes.isTypeNull()) {
+ // There are two 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.
+ sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
+ if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
+ sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
+ }
+ } else {
+ final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
+ if (codePointBeforeCursor == Constants.NOT_A_CODE) {
+ // Nothing to delete before the cursor.
+ return;
+ }
+ final int lengthToDelete =
+ Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
+ mConnection.deleteSurroundingText(lengthToDelete, 0);
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_handleBackspace(lengthToDelete,
+ true /* shouldUncommitLogUnit */);
+ }
+ if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
+ final int codePointBeforeCursorToDeleteAgain =
+ mConnection.getCodePointBeforeCursor();
+ if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
+ final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
+ codePointBeforeCursorToDeleteAgain) ? 2 : 1;
+ mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain,
+ true /* shouldUncommitLogUnit */);
+ }
+ }
+ }
+ }
+ }
+ if (settingsValues.isSuggestionStripVisible()
+ && settingsValues.mCurrentLanguageHasSpaces) {
+ restartSuggestionsOnWordTouchedByCursor(settingsValues,
+ deleteCountAtStart - mDeleteCount /* offset */,
+ true /* includeResumedWordInSuggestions */, keyboardSwitcher);
+ }
+ // We just removed at least one character. We need to update the auto-caps state.
+ keyboardSwitcher.updateShiftState();
+ }
+ }
+
+ /**
+ * Handle a press on the language switch key (the "globe key")
+ */
+ private void handleLanguageSwitchKey() {
+ mLatinIME.switchToNextSubtype();
+ }
+
+ /**
+ * Swap a space with a space-swapping punctuation sign.
+ *
+ * This method will check that there are two characters before the cursor and that the first
+ * one is a space before it does the actual swapping.
+ */
+ // TODO: Remove this argument
+ private void swapSwapperAndSpace(final KeyboardSwitcher keyboardSwitcher) {
+ final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
+ // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
+ if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Constants.CODE_SPACE) {
+ mConnection.deleteSurroundingText(2, 0);
+ final String text = lastTwo.charAt(1) + " ";
+ mConnection.commitText(text, 1);
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text);
+ }
+ keyboardSwitcher.updateShiftState();
+ }
+ }
+
+ /*
+ * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
+ * @param settingsValues The current settings values.
+ * @param codePoint The code point that is about to be inserted.
+ * @param spaceState The space state at start of this batch edit.
+ * @param isFromSuggestionStrip Whether this code point is coming from the suggestion strip.
+ * @return whether we should swap the space instead of removing it.
+ */
+ private boolean maybeStripSpace(final SettingsValues settingsValues,
+ final int code, final int spaceState, final boolean isFromSuggestionStrip) {
+ if (Constants.CODE_ENTER == code && SpaceState.SWAP_PUNCTUATION == spaceState) {
+ mConnection.removeTrailingSpace();
+ return false;
+ }
+ if ((SpaceState.WEAK == spaceState || SpaceState.SWAP_PUNCTUATION == spaceState)
+ && isFromSuggestionStrip) {
+ if (settingsValues.isUsuallyPrecededBySpace(code)) return false;
+ if (settingsValues.isUsuallyFollowedBySpace(code)) return true;
+ mConnection.removeTrailingSpace();
+ }
+ return false;
+ }
+
+ /**
+ * Apply the double-space-to-period transformation if applicable.
+ *
+ * The double-space-to-period transformation means that we replace two spaces with a
+ * period-space sequence of characters. This typically happens when the user presses space
+ * twice in a row quickly.
+ * This method will check that the double-space-to-period is active in settings, that the
+ * two spaces have been input close enough together, and that the previous character allows
+ * for the transformation to take place. If all of these conditions are fulfilled, this
+ * method applies the transformation and returns true. Otherwise, it does nothing and
+ * returns false.
+ *
+ * @param settingsValues the current values of the settings.
+ * @return true if we applied the double-space-to-period transformation, false otherwise.
+ */
+ private boolean maybeDoubleSpacePeriod(final SettingsValues settingsValues,
+ // TODO: remove this argument
+ final LatinIME.UIHandler handler) {
+ if (!settingsValues.mUseDoubleSpacePeriod) return false;
+ if (!handler.isAcceptingDoubleSpacePeriod()) return false;
+ // We only do this when we see two spaces and an accepted code point before the cursor.
+ // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars.
+ final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0);
+ if (null == lastThree) return false;
+ final int length = lastThree.length();
+ if (length < 3) return false;
+ if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false;
+ if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false;
+ // We know there are spaces in pos -1 and -2, and we have at least three chars.
+ // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space,
+ // so this is fine.
+ final int firstCodePoint =
+ Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ?
+ Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3);
+ if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) {
+ handler.cancelDoubleSpacePeriodTimer();
+ mConnection.deleteSurroundingText(2, 0);
+ final String textToInsert = new String(
+ new int[] { settingsValues.mSentenceSeparator, Constants.CODE_SPACE }, 0, 2);
+ mConnection.commitText(textToInsert, 1);
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
+ false /* isBatchMode */);
+ }
+ mWordComposer.discardPreviousWordForSuggestion();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether this code point can be followed by the double-space-to-period transformation.
+ *
+ * See #maybeDoubleSpaceToPeriod for details.
+ * Generally, most word characters can be followed by the double-space-to-period transformation,
+ * while most punctuation can't. Some punctuation however does allow for this to take place
+ * after them, like the closing parenthesis for example.
+ *
+ * @param codePoint the code point after which we may want to apply the transformation
+ * @return whether it's fine to apply the transformation after this code point.
+ */
+ private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) {
+ // TODO: This should probably be a blacklist rather than a whitelist.
+ // TODO: This should probably be language-dependant...
+ return Character.isLetterOrDigit(codePoint)
+ || codePoint == Constants.CODE_SINGLE_QUOTE
+ || codePoint == Constants.CODE_DOUBLE_QUOTE
+ || codePoint == Constants.CODE_CLOSING_PARENTHESIS
+ || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
+ || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
+ || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET
+ || codePoint == Constants.CODE_PLUS
+ || codePoint == Constants.CODE_PERCENT
+ || Character.getType(codePoint) == Character.OTHER_SYMBOL;
+ }
+
+ /**
+ * Performs a recapitalization event.
+ * @param settingsValues The current settings values.
+ */
+ private void performRecapitalization(final SettingsValues settingsValues) {
+ if (mLastSelectionStart == mLastSelectionEnd) {
+ return; // No selection
+ }
+ // If we have a recapitalize in progress, use it; otherwise, create a new one.
+ if (!mRecapitalizeStatus.isActive()
+ || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+ final CharSequence selectedText =
+ mConnection.getSelectedText(0 /* flags, 0 for no styles */);
+ if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
+ mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
+ selectedText.toString(),
+ settingsValues.mLocale, settingsValues.mWordSeparators);
+ // We trim leading and trailing whitespace.
+ mRecapitalizeStatus.trim();
+ // Trimming the object may have changed the length of the string, and we need to
+ // reposition the selection handles accordingly. As this result in an IPC call,
+ // only do it if it's actually necessary, in other words if the recapitalize status
+ // is not set at the same place as before.
+ if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+ mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
+ mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
+ }
+ }
+ mConnection.finishComposingText();
+ mRecapitalizeStatus.rotate();
+ final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
+ mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+ mConnection.deleteSurroundingText(numCharsDeleted, 0);
+ mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
+ mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
+ mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
+ mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
+ }
+
+ private void performAdditionToUserHistoryDictionary(final SettingsValues settingsValues,
+ final String suggestion, final String prevWord) {
+ // If correction is not enabled, we don't add words to the user history dictionary.
+ // That's to avoid unintended additions in some sensitive fields, or fields that
+ // expect to receive non-words.
+ if (!settingsValues.mCorrectionEnabled) return;
+
+ if (TextUtils.isEmpty(suggestion)) return;
+ final Suggest suggest = mSuggest;
+ if (suggest == null) return;
+
+ suggest.mDictionaryFacilitator.addToUserHistory(mWordComposer, prevWord, suggestion);
+ }
+
+ public void performUpdateSuggestionStripSync(final SettingsValues settingsValues,
+ // TODO: Remove this argument
+ final LatinIME.UIHandler handler) {
+ handler.cancelUpdateSuggestionStrip();
+
+ // Check if we have a suggestion engine attached.
+ if (mSuggest == null || !settingsValues.isSuggestionsRequested()) {
+ if (mWordComposer.isComposingWord()) {
+ Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
+ + "requested!");
+ }
+ return;
+ }
+
+ if (!mWordComposer.isComposingWord() && !settingsValues.mBigramPredictionEnabled) {
+ mLatinIME.setPunctuationSuggestions();
+ return;
+ }
+
+ final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>();
+ mInputLogicHandler.getSuggestedWords(Suggest.SESSION_TYPING,
+ SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
+ final SuggestedWords suggestedWordsWithMaybeOlderSuggestions =
+ mLatinIME.maybeRetrieveOlderSuggestions(
+ mWordComposer.getTypedWord(), suggestedWords);
+ holder.set(suggestedWordsWithMaybeOlderSuggestions);
+ }
+ }
+ );
+
+ // This line may cause the current thread to wait.
+ final SuggestedWords suggestedWords = holder.get(null,
+ Constants.GET_SUGGESTED_WORDS_TIMEOUT);
+ if (suggestedWords != null) {
+ mLatinIME.showSuggestionStrip(suggestedWords);
+ }
+ }
+
+ /**
+ * Check if the cursor is touching a word. If so, restart suggestions on this word, else
+ * do nothing.
+ *
+ * @param settingsValues the current values of the settings.
+ * @param offset how much the cursor is expected to have moved since the last updateSelection.
+ * @param includeResumedWordInSuggestions whether to include the word on which we resume
+ * suggestions in the suggestion list.
+ */
+ // TODO: make this private.
+ public void restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues,
+ final int offset, final boolean includeResumedWordInSuggestions,
+ // TODO: Remove this argument.
+ final KeyboardSwitcher keyboardSwitcher) {
+ // HACK: We may want to special-case some apps that exhibit bad behavior in case of
+ // recorrection. This is a temporary, stopgap measure that will be removed later.
+ // TODO: remove this.
+ if (settingsValues.isBrokenByRecorrection()) return;
+ // A simple way to test for support from the TextView.
+ if (!mLatinIME.isSuggestionsStripVisible()) return;
+ // Recorrection is not supported in languages without spaces because we don't know
+ // how to segment them yet.
+ if (!settingsValues.mCurrentLanguageHasSpaces) return;
+ // If the cursor is not touching a word, or if there is a selection, return right away.
+ if (mLastSelectionStart != mLastSelectionEnd) return;
+ // If we don't know the cursor location, return.
+ if (mLastSelectionStart < 0) return;
+ final int expectedCursorPosition = mLastSelectionStart + offset; // We know Start == End
+ if (!mConnection.isCursorTouchingWord(settingsValues)) return;
+ final TextRange range = mConnection.getWordRangeAtCursor(
+ settingsValues.mWordSeparators, 0 /* additionalPrecedingWordsCount */);
+ if (null == range) return; // Happens if we don't have an input connection at all
+ if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out.
+ // If for some strange reason (editor bug or so) we measure the text before the cursor as
+ // longer than what the entire text is supposed to be, the safe thing to do is bail out.
+ final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor();
+ if (numberOfCharsInWordBeforeCursor > expectedCursorPosition) return;
+ final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
+ final String typedWord = range.mWord.toString();
+ if (includeResumedWordInSuggestions) {
+ suggestions.add(new SuggestedWordInfo(typedWord,
+ SuggestionStripView.MAX_SUGGESTIONS + 1,
+ SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+ SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
+ }
+ if (!isResumableWord(settingsValues, typedWord)) return;
+ int i = 0;
+ for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) {
+ for (final String s : span.getSuggestions()) {
+ ++i;
+ if (!TextUtils.equals(s, typedWord)) {
+ suggestions.add(new SuggestedWordInfo(s,
+ SuggestionStripView.MAX_SUGGESTIONS - i,
+ SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+ SuggestedWordInfo.NOT_A_CONFIDENCE
+ /* autoCommitFirstWordConfidence */));
+ }
+ }
+ }
+ mWordComposer.setComposingWord(typedWord,
+ getNthPreviousWordForSuggestion(settingsValues,
+ // 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.
+ 0 == numberOfCharsInWordBeforeCursor ? 1 : 2),
+ keyboardSwitcher.getKeyboard());
+ mWordComposer.setCursorPositionWithinWord(
+ typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
+ // TODO: Change these two lines to setComposingRegion(cursorPosition,
+ // cursorPosition + range.getNumberOfCharsInWordAfterCursor());
+ mConnection.deleteSurroundingText(numberOfCharsInWordBeforeCursor,
+ typedWord.length() - numberOfCharsInWordBeforeCursor);
+ mConnection.setComposingText(typedWord, 1);
+ if (suggestions.isEmpty()) {
+ // We come here if there weren't any suggestion spans on this word. We will try to
+ // compute suggestions for it instead.
+ mInputLogicHandler.getSuggestedWords(Suggest.SESSION_TYPING,
+ SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(
+ final SuggestedWords suggestedWordsIncludingTypedWord) {
+ final SuggestedWords suggestedWords;
+ if (suggestedWordsIncludingTypedWord.size() > 1
+ && !includeResumedWordInSuggestions) {
+ // 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 #getSuggestedWordsExcludingTypedWord() method sets
+ // willAutoCorrect to false.
+ suggestedWords = suggestedWordsIncludingTypedWord
+ .getSuggestedWordsExcludingTypedWord();
+ } 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;
+ }
+ // We need to pass typedWord because mWordComposer.mTypedWord may
+ // differ from typedWord.
+ mLatinIME.unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
+ suggestedWords, typedWord);
+ }});
+ } else {
+ // We found suggestion spans in the word. We'll create the SuggestedWords out of
+ // them, and make willAutoCorrect false.
+ final SuggestedWords suggestedWords = new SuggestedWords(suggestions,
+ true /* typedWordValid */, false /* willAutoCorrect */,
+ false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
+ false /* isPrediction */);
+ // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord.
+ mLatinIME.unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords,
+ typedWord);
+ }
+ }
+
+ /**
+ * Reverts a previous commit with auto-correction.
+ *
+ * This is triggered upon pressing backspace just after a commit with auto-correction.
+ *
+ * @param settingsValues the current settings values.
+ */
+ private void revertCommit(final SettingsValues settingsValues,
+ // TODO: remove these arguments
+ final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+ final String previousWord = mLastComposedWord.mPrevWord;
+ final String originallyTypedWord = mLastComposedWord.mTypedWord;
+ final String committedWord = mLastComposedWord.mCommittedWord;
+ final int cancelLength = committedWord.length();
+ // We want java chars, not codepoints for the following.
+ final int separatorLength = mLastComposedWord.mSeparatorString.length();
+ // TODO: should we check our saved separator against the actual contents of the text view?
+ final int deleteLength = cancelLength + separatorLength;
+ if (LatinImeLogger.sDBG) {
+ if (mWordComposer.isComposingWord()) {
+ throw new RuntimeException("revertCommit, but we are composing a word");
+ }
+ final CharSequence wordBeforeCursor =
+ mConnection.getTextBeforeCursor(deleteLength, 0).subSequence(0, cancelLength);
+ if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
+ throw new RuntimeException("revertCommit check failed: we thought we were "
+ + "reverting \"" + committedWord
+ + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
+ }
+ }
+ mConnection.deleteSurroundingText(deleteLength, 0);
+ if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
+ if (mSuggest != null) {
+ mSuggest.mDictionaryFacilitator.cancelAddingUserHistory(
+ previousWord, committedWord);
+ }
+ }
+ final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
+ if (settingsValues.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.
+ mConnection.commitText(stringToCommit, 1);
+ } else {
+ // For languages without spaces, we revert the typed string but the cursor is flush
+ // with the typed word, so we need to resume suggestions right away.
+ mWordComposer.setComposingWord(stringToCommit, previousWord,
+ keyboardSwitcher.getKeyboard());
+ mConnection.setComposingText(stringToCommit, 1);
+ }
+ if (settingsValues.mIsInternal) {
+ LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
+ Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+ }
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord,
+ mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString);
+ }
+ // Don't restart suggestion yet. We'll restart if the user deletes the
+ // separator.
+ mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+ // We have a separator between the word and the cursor: we should show predictions.
+ handler.postUpdateSuggestionStrip();
+ }
+
+ /**
+ * Factor in auto-caps and manual caps and compute the current caps mode.
+ * @param settingsValues the current settings values.
+ * @param keyboardShiftMode the current shift mode of the keyboard. See
+ * KeyboardSwitcher#getKeyboardShiftMode() for possible values.
+ * @return the actual caps mode the keyboard is in right now.
+ */
+ private int getActualCapsMode(final SettingsValues settingsValues,
+ final int keyboardShiftMode) {
+ if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
+ final int auto = getCurrentAutoCapsState(settingsValues);
+ if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
+ return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
+ }
+ if (0 != auto) {
+ return WordComposer.CAPS_MODE_AUTO_SHIFTED;
+ }
+ return WordComposer.CAPS_MODE_OFF;
+ }
+
+ /**
+ * Gets the current auto-caps state, factoring in the space state.
+ *
+ * This method tries its best to do this in the most efficient possible manner. It avoids
+ * getting text from the editor if possible at all.
+ * This is called from the KeyboardSwitcher (through a trampoline in LatinIME) because it
+ * needs to know auto caps state to display the right layout.
+ *
+ * @param optionalSettingsValues settings values, or null if we should just get the current ones
+ * from the singleton.
+ * @return a caps mode from TextUtils.CAP_MODE_* or Constants.TextUtils.CAP_MODE_OFF.
+ */
+ public int getCurrentAutoCapsState(final SettingsValues optionalSettingsValues) {
+ // If we are in a batch edit, we need to use the same settings values as the outside
+ // code, that will pass it to us. Otherwise, we can just take the current values.
+ final SettingsValues settingsValues = null != optionalSettingsValues
+ ? optionalSettingsValues : Settings.getInstance().getCurrent();
+ if (!settingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
+
+ final EditorInfo ei = getCurrentInputEditorInfo();
+ if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
+ final int inputType = ei.inputType;
+ // Warning: this depends on mSpaceState, which may not be the most current value. If
+ // mSpaceState gets updated later, whoever called this may need to be told about it.
+ return mConnection.getCursorCapsMode(inputType, settingsValues,
+ SpaceState.PHANTOM == mSpaceState);
+ }
+
+ public int getCurrentRecapitalizeState() {
+ if (!mRecapitalizeStatus.isActive()
+ || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+ // Not recapitalizing at the moment
+ return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
+ }
+ return mRecapitalizeStatus.getCurrentMode();
+ }
+
+ /**
+ * @return the editor info for the current editor
+ */
+ private EditorInfo getCurrentInputEditorInfo() {
+ return mLatinIME.getCurrentInputEditorInfo();
+ }
+
+ /**
+ * Get the nth previous word before the cursor as context for the suggestion process.
+ * @param currentSettings the current settings values.
+ * @param nthPreviousWord reverse index of the word to get (1-indexed)
+ * @return the nth previous word before the cursor.
+ */
+ // TODO: Make this private
+ public String getNthPreviousWordForSuggestion(final SettingsValues currentSettings,
+ final int nthPreviousWord) {
+ if (currentSettings.mCurrentLanguageHasSpaces) {
+ // If we are typing in a language with spaces we can just look up the previous
+ // word from textview.
+ return mConnection.getNthPreviousWord(currentSettings, nthPreviousWord);
+ } else {
+ return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
+ : mLastComposedWord.mCommittedWord;
+ }
+ }
+
+ /**
+ * Tests the passed word for resumability.
+ *
+ * We can resume suggestions on words whose first code point is a word code point (with some
+ * nuances: check the code for details).
+ *
+ * @param settings the current values of the settings.
+ * @param word the word to evaluate.
+ * @return whether it's fine to resume suggestions on this word.
+ */
+ private static boolean isResumableWord(final SettingsValues settings, final String word) {
+ final int firstCodePoint = word.codePointAt(0);
+ return settings.isWordCodePoint(firstCodePoint)
+ && Constants.CODE_SINGLE_QUOTE != firstCodePoint
+ && Constants.CODE_DASH != firstCodePoint;
+ }
+
+ /**
+ * @param actionId the action to perform
+ */
+ private void performEditorAction(final int actionId) {
+ mConnection.performEditorAction(actionId);
+ }
+
+ /**
+ * Perform the processing specific to inputting TLDs.
+ *
+ * Some keys input a TLD (specifically, the ".com" key) and this warrants some specific
+ * processing. First, if this is a TLD, we ignore PHANTOM spaces -- this is done by type
+ * of character in onCodeInput, but since this gets inputted as a whole string we need to
+ * do it here specifically. Then, if the last character before the cursor is a period, then
+ * we cut the dot at the start of ".com". This is because humans tend to type "www.google."
+ * and then press the ".com" key and instinctively don't expect to get "www.google..com".
+ *
+ * @param text the raw text supplied to onTextInput
+ * @return the text to actually send to the editor
+ */
+ private String performSpecificTldProcessingOnTextInput(final String text) {
+ if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
+ || !Character.isLetter(text.charAt(1))) {
+ // Not a tld: do nothing.
+ return text;
+ }
+ // We have a TLD (or something that looks like this): make sure we don't add
+ // a space even if currently in phantom mode.
+ mSpaceState = SpaceState.NONE;
+ final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
+ // If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT.
+ if (Constants.CODE_PERIOD == codePointBeforeCursor) {
+ return text.substring(1);
+ } else {
+ return text;
+ }
+ }
+
+ /**
+ * Handle a press on the settings key.
+ */
+ private void onSettingsKeyPressed() {
+ mLatinIME.displaySettingsDialog();
+ }
+
+ /**
+ * Resets the whole input state to the starting state.
+ *
+ * This will clear the composing word, reset the last composed word, clear the suggestion
+ * strip and tell the input connection about it so that it can refresh its caches.
+ *
+ * @param settingsValues the current values of the settings.
+ * @param newSelStart the new selection start, in java characters.
+ * @param newSelEnd the new selection end, in java characters.
+ */
+ // TODO: how is this different from startInput ?!
+ // TODO: remove all references to this in LatinIME and make this private
+ public void resetEntireInputState(final SettingsValues settingsValues,
+ final int newSelStart, final int newSelEnd) {
+ final boolean shouldFinishComposition = mWordComposer.isComposingWord();
+ resetComposingState(true /* alsoResetLastComposedWord */);
+ if (settingsValues.mBigramPredictionEnabled) {
+ mLatinIME.clearSuggestionStrip();
+ } else {
+ mLatinIME.setSuggestedWords(settingsValues.mSuggestPuncList);
+ }
+ mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd,
+ shouldFinishComposition);
+ }
+
+ /**
+ * Resets only the composing state.
+ *
+ * Compare #resetEntireInputState, which also clears the suggestion strip and resets the
+ * input connection caches. This only deals with the composing state.
+ *
+ * @param alsoResetLastComposedWord whether to also reset the last composed word.
+ */
+ // TODO: remove all references to this in LatinIME and make this private.
+ public void resetComposingState(final boolean alsoResetLastComposedWord) {
+ mWordComposer.reset();
+ if (alsoResetLastComposedWord) {
+ mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+ }
+ }
+
+ /**
+ * Gets a chunk of text with or the auto-correction indicator underline span as appropriate.
+ *
+ * This method looks at the old state of the auto-correction indicator to put or not put
+ * the underline span as appropriate. It is important to note that this does not correspond
+ * exactly to whether this word will be auto-corrected to or not: what's important here is
+ * to keep the same indication as before.
+ * When we add a new code point to a composing word, we don't know yet if we are going to
+ * auto-correct it until the suggestions are computed. But in the mean time, we still need
+ * to display the character and to extend the previous underline. To avoid any flickering,
+ * the underline should keep the same color it used to have, even if that's not ultimately
+ * the correct color for this new word. When the suggestions are finished evaluating, we
+ * will call this method again to fix the color of the underline.
+ *
+ * @param text the text on which to maybe apply the span.
+ * @return the same text, with the auto-correction underline span if that's appropriate.
+ */
+ // TODO: remove all references to this in LatinIME and make this private. Also, shouldn't
+ // this go in some *Utils class instead?
+ public CharSequence getTextWithUnderline(final String text) {
+ return mIsAutoCorrectionIndicatorOn
+ ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(mLatinIME, text)
+ : text;
+ }
+
+ /**
+ * Sends a DOWN key event followed by an UP key event to the editor.
+ *
+ * If possible at all, avoid using this method. It causes all sorts of race conditions with
+ * the text view because it goes through a different, asynchronous binder. Also, batch edits
+ * are ignored for key events. Use the normal software input methods instead.
+ *
+ * @param keyCode the key code to send inside the key event.
+ */
+ private void sendDownUpKeyEvent(final int keyCode) {
+ final long eventTime = SystemClock.uptimeMillis();
+ mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
+ KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
+ mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+ KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
+ }
+
+ /**
+ * Sends a code point to the editor, using the most appropriate method.
+ *
+ * Normally we send code points with commitText, but there are some cases (where backward
+ * compatibility is a concern for example) where we want to use deprecated methods.
+ *
+ * @param settingsValues the current values of the settings.
+ * @param codePoint the code point to send.
+ */
+ private void sendKeyCodePoint(final SettingsValues settingsValues, final int codePoint) {
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_sendKeyCodePoint(codePoint);
+ }
+ // TODO: Remove this special handling of digit letters.
+ // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
+ if (codePoint >= '0' && codePoint <= '9') {
+ sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0);
+ return;
+ }
+
+ // TODO: we should do this also when the editor has TYPE_NULL
+ if (Constants.CODE_ENTER == codePoint && settingsValues.isBeforeJellyBean()) {
+ // 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.
+ sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER);
+ } else {
+ mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1);
+ }
+ }
+
+ /**
+ * Promote a phantom space to an actual space.
+ *
+ * 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.
+ *
+ * @param settingsValues the current values of the settings.
+ */
+ // TODO: Make this private.
+ public void promotePhantomSpace(final SettingsValues settingsValues) {
+ if (settingsValues.shouldInsertSpacesAutomatically()
+ && settingsValues.mCurrentLanguageHasSpaces
+ && !mConnection.textBeforeCursorLooksLikeURL()) {
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_promotePhantomSpace();
+ }
+ sendKeyCodePoint(settingsValues, Constants.CODE_SPACE);
+ }
+ }
+
+ /**
+ * Do the final processing after a batch input has ended. This commits the word to the editor.
+ * @param settingsValues the current values of the settings.
+ * @param suggestedWords suggestedWords to use.
+ */
+ public void endBatchInputAsyncInternal(final SettingsValues settingsValues,
+ final SuggestedWords suggestedWords,
+ // TODO: remove this argument
+ 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);
+ }
+ if (settingsValues.mPhraseGestureEnabled) {
+ // Find the last space
+ final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
+ if (0 != indexOfLastSpace) {
+ mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
+ mLatinIME.showSuggestionStrip(
+ suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture());
+ }
+ final String lastWord = batchInputText.substring(indexOfLastSpace);
+ mWordComposer.setBatchInputWord(lastWord);
+ mConnection.setComposingText(lastWord, 1);
+ } else {
+ mWordComposer.setBatchInputWord(batchInputText);
+ mConnection.setComposingText(batchInputText, 1);
+ }
+ mConnection.endBatchEdit();
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords);
+ }
+ // Space state must be updated before calling updateShiftState
+ mSpaceState = SpaceState.PHANTOM;
+ keyboardSwitcher.updateShiftState();
+ }
+
+ /**
+ * Commit the typed string to the editor.
+ *
+ * This is typically called when we should commit the currently composing word without applying
+ * auto-correction to it. Typically, we come here upon pressing a separator when the keyboard
+ * is configured to not do auto-correction at all (because of the settings or the properties of
+ * the editor). In this case, `separatorString' is set to the separator that was pressed.
+ * We also come here in a variety of cases with external user action. For example, when the
+ * cursor is moved while there is a composition, or when the keyboard is closed, or when the
+ * user presses the Send button for an SMS, we don't auto-correct as that would be unexpected.
+ * In this case, `separatorString' is set to NOT_A_SEPARATOR.
+ *
+ * @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) {
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode());
+ }
+ commitChosenWord(settingsValues, typedWord,
+ LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString);
+ }
+ }
+
+ /**
+ * Commit the current auto-correction.
+ *
+ * This will commit the best guess of the keyboard regarding what the user meant by typing
+ * the currently composing word. The IME computes suggestions and assigns a confidence score
+ * to each of them; when it's confident enough in one suggestion, it replaces the typed string
+ * by this suggestion at commit time. When it's not confident enough, or when it has no
+ * suggestions, or when the settings or environment does not allow for auto-correction, then
+ * this method just commits the typed string.
+ * Note that if suggestions are currently being computed in the background, this method will
+ * block until the computation returns. This is necessary for consistency (it would be very
+ * strange if pressing space would commit a different word depending on how fast you press).
+ *
+ * @param settingsValues the current value of the settings.
+ * @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) {
+ // Complete any pending suggestions query first
+ if (handler.hasPendingUpdateSuggestions()) {
+ performUpdateSuggestionStripSync(settingsValues, handler);
+ }
+ final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
+ final String typedWord = mWordComposer.getTypedWord();
+ final String autoCorrection = (typedAutoCorrection != null)
+ ? typedAutoCorrection : typedWord;
+ if (autoCorrection != null) {
+ if (TextUtils.isEmpty(typedWord)) {
+ throw new RuntimeException("We have an auto-correction but the typed word "
+ + "is empty? Impossible! I must commit suicide.");
+ }
+ if (settingsValues.mIsInternal) {
+ LatinImeLoggerUtils.onAutoCorrection(
+ typedWord, autoCorrection, separator, mWordComposer);
+ }
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ final SuggestedWords suggestedWords = mSuggestedWords;
+ ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
+ separator, mWordComposer.isBatchMode(), suggestedWords);
+ }
+ commitChosenWord(settingsValues, autoCorrection,
+ LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator);
+ if (!typedWord.equals(autoCorrection)) {
+ // This will make the correction flash for a short while as a visual clue
+ // to the user that auto-correction happened. It has no other effect; in particular
+ // note that this won't affect the text inside the text field AT ALL: it only makes
+ // the segment of text starting at the supplied index and running for the length
+ // of the auto-correction flash. At this moment, the "typedWord" argument is
+ // ignored by TextView.
+ mConnection.commitCorrection(
+ new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
+ typedWord, autoCorrection));
+ }
+ }
+ }
+
+ /**
+ * Commits the chosen word to the text field and saves it for later retrieval.
+ *
+ * @param settingsValues the current values of the settings.
+ * @param chosenWord the word we want to commit.
+ * @param commitType the type of the commit, as one of LastComposedWord.COMMIT_TYPE_*
+ * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none.
+ */
+ // TODO: Make this private
+ public void commitChosenWord(final SettingsValues settingsValues, final String chosenWord,
+ final int commitType, final String separatorString) {
+ final SuggestedWords suggestedWords = mSuggestedWords;
+ mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord,
+ suggestedWords), 1);
+ // TODO: we pass 2 here, but would it be better to move this above and pass 1 instead?
+ final String prevWord = mConnection.getNthPreviousWord(settingsValues, 2);
+ // Add the word to the user history dictionary
+ performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWord);
+ // 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,
+ chosenWord, separatorString, prevWord);
+ final boolean shouldDiscardPreviousWordForSuggestion;
+ if (0 == StringUtils.codePointCount(separatorString)) {
+ // Separator is 0-length, we can keep the previous word for suggestion. Either this
+ // was a manual pick or the language has no spaces in which case we want to keep the
+ // previous word, or it was the keyboard closing or the cursor moving in which case it
+ // will be reset anyway.
+ shouldDiscardPreviousWordForSuggestion = false;
+ } else {
+ // Otherwise, we discard if the separator contains any non-whitespace.
+ shouldDiscardPreviousWordForSuggestion =
+ !StringUtils.containsOnlyWhitespace(separatorString);
+ }
+ if (shouldDiscardPreviousWordForSuggestion) {
+ mWordComposer.discardPreviousWordForSuggestion();
+ }
+ }
+
+ /**
+ * 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
+ * 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.
+ */
+ private void tryFixLyingCursorPosition() {
+ final CharSequence textBeforeCursor = mConnection.getTextBeforeCursor(
+ Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
+ if (null == textBeforeCursor) {
+ mLastSelectionStart = mLastSelectionEnd = Constants.NOT_A_CURSOR_POSITION;
+ } else {
+ final int textLength = textBeforeCursor.length();
+ if (textLength > mLastSelectionStart
+ || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
+ && mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
+ // It should not be possible to have only one of those variables be
+ // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized
+ // (simple cursor, no selection) or there is no cursor/we don't know its pos
+ final boolean wasEqual = mLastSelectionStart == mLastSelectionEnd;
+ mLastSelectionStart = textLength;
+ // We can't figure out the value of mLastSelectionEnd :(
+ // But at least if it's smaller than mLastSelectionStart something is wrong,
+ // and if they used to be equal we also don't want to make it look like there is a
+ // selection.
+ if (wasEqual || mLastSelectionStart > mLastSelectionEnd) {
+ mLastSelectionEnd = mLastSelectionStart;
+ }
+ }
+ }
+ }
+
+ /**
+ * Retry resetting caches in the rich input connection.
+ *
+ * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
+ * This method handles the retry, and re-schedules a new retry if we still can't access.
+ * We only retry up to 5 times before giving up.
+ *
+ * @param settingsValues the current values of the settings.
+ * @param tryResumeSuggestions Whether we should resume suggestions or not.
+ * @param remainingTries How many times we may try again before giving up.
+ */
+ // TODO: make this private
+ public void retryResetCaches(final SettingsValues settingsValues,
+ final boolean tryResumeSuggestions, final int remainingTries,
+ // TODO: remove these arguments
+ final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+ if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(
+ mLastSelectionStart, mLastSelectionEnd, false)) {
+ if (0 < remainingTries) {
+ handler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
+ return;
+ }
+ // If remainingTries is 0, we should stop waiting for new tries, but it's still
+ // better to load the keyboard (less things will be broken).
+ }
+ tryFixLyingCursorPosition();
+ keyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), settingsValues);
+ if (tryResumeSuggestions) {
+ handler.postResumeSuggestions();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
new file mode 100644
index 000000000..3258dcdfb
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
@@ -0,0 +1,172 @@
+/*
+ * 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.inputlogic;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+
+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;
+
+/**
+ * A helper to manage deferred tasks for the input logic.
+ */
+// TODO: Make this package private
+public class InputLogicHandler implements Handler.Callback {
+ final Handler mNonUIThreadHandler;
+ // TODO: remove this reference.
+ final LatinIME mLatinIME;
+ final InputLogic mInputLogic;
+ private final Object mLock = new Object();
+ private boolean mInBatchInput; // synchronized using {@link #mLock}.
+
+ private static final int MSG_GET_SUGGESTED_WORDS = 1;
+
+ public InputLogicHandler(final LatinIME latinIME, final InputLogic inputLogic) {
+ final HandlerThread handlerThread = new HandlerThread(
+ InputLogicHandler.class.getSimpleName());
+ handlerThread.start();
+ mNonUIThreadHandler = new Handler(handlerThread.getLooper(), this);
+ mLatinIME = latinIME;
+ mInputLogic = inputLogic;
+ }
+
+ public void destroy() {
+ mNonUIThreadHandler.getLooper().quit();
+ }
+
+ /**
+ * Handle a message.
+ * @see android.os.Handler.Callback#handleMessage(android.os.Message)
+ */
+ // Called on the Non-UI handler thread by the Handler code.
+ @Override
+ public boolean handleMessage(final Message msg) {
+ switch (msg.what) {
+ case MSG_GET_SUGGESTED_WORDS:
+ mLatinIME.getSuggestedWords(msg.arg1 /* sessionId */,
+ msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj);
+ break;
+ }
+ return true;
+ }
+
+ // Called on the UI thread by InputLogic.
+ public void onStartBatchInput() {
+ synchronized (mLock) {
+ mInBatchInput = true;
+ }
+ }
+
+ /**
+ * Fetch suggestions corresponding to an update of a batch input.
+ * @param batchPointers the updated pointers, including the part that was passed last time.
+ * @param sequenceNumber the sequence number associated with this batch input.
+ * @param forEnd true if this is the end of a batch input, false if it's an update.
+ */
+ // This method can be called from any thread and will see to it that the correct threads
+ // are used for parts that require it. This method will send a message to the Non-UI handler
+ // thread to pull suggestions, and get the inlined callback to get called on the Non-UI
+ // handler thread. If this is the end of a batch input, the callback will then proceed to
+ // send a message to the UI handler in LatinIME so that showing suggestions can be done on
+ // the UI thread.
+ private void updateBatchInput(final InputPointers batchPointers,
+ final int sequenceNumber, final boolean forEnd) {
+ synchronized (mLock) {
+ if (!mInBatchInput) {
+ // Batch input has ended or canceled while the message was being delivered.
+ return;
+ }
+ mInputLogic.mWordComposer.setBatchInputPointers(batchPointers);
+ getSuggestedWords(Suggest.SESSION_GESTURE, 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,
+ forEnd /* dismissGestureFloatingPreviewText */);
+ if (forEnd) {
+ mInBatchInput = false;
+ // The following call schedules onEndBatchInputAsyncInternal
+ // to be called on the UI thread.
+ mLatinIME.mHandler.onEndBatchInput(suggestedWords);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Update a batch input.
+ *
+ * This fetches suggestions and updates the suggestion strip and the floating text preview.
+ *
+ * @param batchPointers the updated batch pointers.
+ * @param sequenceNumber the sequence number associated with this batch input.
+ */
+ // Called on the UI thread by InputLogic.
+ public void onUpdateBatchInput(final InputPointers batchPointers,
+ final int sequenceNumber) {
+ updateBatchInput(batchPointers, sequenceNumber, false /* forEnd */);
+ }
+
+ /**
+ * Cancel a batch input.
+ *
+ * Note that as opposed to onEndBatchInput, we do the UI side of this immediately on the
+ * same thread, rather than get this to call a method in LatinIME. This is because
+ * canceling a batch input does not necessitate the long operation of pulling suggestions.
+ */
+ // Called on the UI thread by InputLogic.
+ public void onCancelBatchInput() {
+ synchronized (mLock) {
+ mInBatchInput = false;
+ }
+ }
+
+ /**
+ * Finish a batch input.
+ *
+ * This fetches suggestions, updates the suggestion strip and commits the first suggestion.
+ * It also dismisses the floating text preview.
+ *
+ * @param batchPointers the updated batch pointers.
+ * @param sequenceNumber the sequence number associated with this batch input.
+ */
+ // Called on the UI thread by InputLogic.
+ public void onEndBatchInput(final InputPointers batchPointers, final int sequenceNumber) {
+ updateBatchInput(batchPointers, sequenceNumber, true /* forEnd */);
+ }
+
+ public void getSuggestedWords(final int sessionId, final int sequenceNumber,
+ final OnGetSuggestedWordsCallback callback) {
+ mNonUIThreadHandler.obtainMessage(
+ MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback).sendToTarget();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java b/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java
new file mode 100644
index 000000000..ce80c0016
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java
@@ -0,0 +1,54 @@
+/*
+ * 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.inputlogic;
+
+/**
+ * Class for managing space states.
+ *
+ * At any given time, the input logic is in one of five possible space states. Depending on the
+ * current space state, some behavior will change; the prime example of this is the PHANTOM state,
+ * in which any subsequent letter input will input a space before the letter. Read on the
+ * description inside this class for each of the space states.
+ */
+public class SpaceState {
+ // None: the state where all the keyboard behavior is the most "standard" and no automatic
+ // input is added or removed. In this state, all self-inserting keys only insert themselves,
+ // and backspace removes one character.
+ public static final int NONE = 0;
+ // Double space: the state where the user pressed space twice quickly, which LatinIME
+ // resolved as period-space. In this state, pressing backspace will undo the
+ // double-space-to-period insertion: it will replace ". " with " ".
+ public static final int DOUBLE = 1;
+ // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip
+ // have just been swapped. In this state, pressing backspace will undo the swap: the
+ // characters will be swapped back back, and the space state will go to WEAK.
+ public static final int SWAP_PUNCTUATION = 2;
+ // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak
+ // spaces happen when the user presses space, accepting the current suggestion (whether
+ // it's an auto-correction or not). In this state, pressing a punctuation from the suggestion
+ // strip inserts it before the space (while it inserts it after the space in the NONE state).
+ public static final int WEAK = 3;
+ // Phantom space: a not-yet-inserted space that should get inserted on the next input,
+ // character provided it's not a separator. If it's a separator, the phantom space is dropped.
+ // Phantom spaces happen when a user chooses a word from the suggestion strip. In this state,
+ // non-separators insert a space before they get inserted.
+ public static final int PHANTOM = 4;
+
+ private SpaceState() {
+ // This class is not publicly instantiable.
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
index fda97dafc..f8fa68f45 100644
--- a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
@@ -32,36 +32,35 @@ import java.util.TreeMap;
* A base class of the binary dictionary decoder.
*/
public abstract class AbstractDictDecoder implements DictDecoder {
- protected FileHeader readHeader(final DictBuffer dictBuffer)
+ private static final int SUCCESS = 0;
+ private static final int ERROR_CANNOT_READ = 1;
+ private static final int ERROR_WRONG_FORMAT = 2;
+
+ protected FileHeader readHeader(final DictBuffer headerBuffer)
throws IOException, UnsupportedFormatException {
- if (dictBuffer == null) {
+ if (headerBuffer == null) {
openDictBuffer();
}
- final int version = HeaderReader.readVersion(dictBuffer);
+ final int version = HeaderReader.readVersion(headerBuffer);
if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION
|| version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) {
throw new UnsupportedFormatException("Unsupported version : " + version);
}
// TODO: Remove this field.
- final int optionsFlags = HeaderReader.readOptionFlags(dictBuffer);
-
- final int headerSize = HeaderReader.readHeaderSize(dictBuffer);
-
+ final int optionsFlags = HeaderReader.readOptionFlags(headerBuffer);
+ final int headerSize = HeaderReader.readHeaderSize(headerBuffer);
if (headerSize < 0) {
throw new UnsupportedFormatException("header size can't be negative.");
}
- final HashMap<String, String> attributes = HeaderReader.readAttributes(dictBuffer,
+ final HashMap<String, String> attributes = HeaderReader.readAttributes(headerBuffer,
headerSize);
final FileHeader header = new FileHeader(headerSize,
- new FusionDictionary.DictionaryOptions(attributes,
- 0 != (optionsFlags & FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG),
- 0 != (optionsFlags & FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG)),
- new FormatOptions(version,
- 0 != (optionsFlags & FormatSpec.SUPPORTS_DYNAMIC_UPDATE),
- 0 != (optionsFlags & FormatSpec.CONTAINS_TIMESTAMP_FLAG)));
+ new FusionDictionary.DictionaryOptions(attributes),
+ new FormatOptions(version,
+ 0 != (optionsFlags & FormatSpec.CONTAINS_TIMESTAMP_FLAG)));
return header;
}
@@ -204,4 +203,25 @@ public abstract class AbstractDictDecoder implements DictDecoder {
return readLength;
}
}
+
+ /**
+ * Check whether the header contains the expected information. This is a no-error method,
+ * that will return an error code and never throw a checked exception.
+ * @return an error code, either ERROR_* or SUCCESS.
+ */
+ private int checkHeader() {
+ try {
+ readHeader();
+ } catch (IOException e) {
+ return ERROR_CANNOT_READ;
+ } catch (UnsupportedFormatException e) {
+ return ERROR_WRONG_FORMAT;
+ }
+ return SUCCESS;
+ }
+
+ @Override
+ public boolean hasValidRawBinaryDictionary() {
+ return checkHeader() == SUCCESS;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
index 216492b4d..9a24c47af 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
@@ -24,12 +24,9 @@ import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Map;
import java.util.TreeMap;
@@ -61,6 +58,7 @@ public final class BinaryDictDecoderUtils {
public int readInt();
public int position();
public void position(int newPosition);
+ @UsedForTesting
public void put(final byte b);
public int limit();
@UsedForTesting
@@ -169,6 +167,15 @@ public final class BinaryDictDecoderUtils {
return size;
}
+ @UsedForTesting
+ static int getCharArraySize(final int[] chars, final int start, final int end) {
+ int size = 0;
+ for (int i = start; i < end; ++i) {
+ size += getCharSize(chars[i]);
+ }
+ return size;
+ }
+
/**
* Writes a char array to a byte buffer.
*
@@ -200,8 +207,7 @@ public final class BinaryDictDecoderUtils {
* @param word the string to write.
* @return the size written, in bytes.
*/
- static int writeString(final byte[] buffer, final int origin,
- final String word) {
+ static int writeString(final byte[] buffer, final int origin, final String word) {
final int length = word.length();
int index = origin;
for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
@@ -223,22 +229,63 @@ public final class BinaryDictDecoderUtils {
*
* This will also write the terminator byte.
*
- * @param buffer the OutputStream to write to.
+ * @param stream the OutputStream to write to.
* @param word the string to write.
+ * @return the size written, in bytes.
*/
- static void writeString(final OutputStream buffer, final String word) throws IOException {
+ static int writeString(final OutputStream stream, final String word) throws IOException {
final int length = word.length();
+ int written = 0;
for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
final int codePoint = word.codePointAt(i);
- if (1 == getCharSize(codePoint)) {
- buffer.write((byte) codePoint);
+ final int charSize = getCharSize(codePoint);
+ if (1 == charSize) {
+ stream.write((byte) codePoint);
+ } else {
+ stream.write((byte) (0xFF & (codePoint >> 16)));
+ stream.write((byte) (0xFF & (codePoint >> 8)));
+ stream.write((byte) (0xFF & codePoint));
+ }
+ written += charSize;
+ }
+ stream.write(FormatSpec.PTNODE_CHARACTERS_TERMINATOR);
+ written += FormatSpec.PTNODE_TERMINATOR_SIZE;
+ return written;
+ }
+
+ /**
+ * Writes an array of code points with our character format to an OutputStream.
+ *
+ * This will also write the terminator byte.
+ *
+ * @param stream the OutputStream to write to.
+ * @param codePoints the array of code points
+ * @return the size written, in bytes.
+ */
+ // TODO: Merge this method with writeCharArray and rename the various write* methods to
+ // make the difference clear.
+ @UsedForTesting
+ static int writeCodePoints(final OutputStream stream, final int[] codePoints,
+ final int startIndex, final int endIndex)
+ throws IOException {
+ int written = 0;
+ for (int i = startIndex; i < endIndex; ++i) {
+ final int codePoint = codePoints[i];
+ final int charSize = getCharSize(codePoint);
+ if (1 == charSize) {
+ stream.write((byte) codePoint);
} else {
- buffer.write((byte) (0xFF & (codePoint >> 16)));
- buffer.write((byte) (0xFF & (codePoint >> 8)));
- buffer.write((byte) (0xFF & codePoint));
+ stream.write((byte) (0xFF & (codePoint >> 16)));
+ stream.write((byte) (0xFF & (codePoint >> 8)));
+ stream.write((byte) (0xFF & codePoint));
}
+ written += charSize;
+ }
+ if (endIndex - startIndex > 1) {
+ stream.write(FormatSpec.PTNODE_CHARACTERS_TERMINATOR);
+ written += FormatSpec.PTNODE_TERMINATOR_SIZE;
}
- buffer.write(FormatSpec.PTNODE_CHARACTERS_TERMINATOR);
+ return written;
}
/**
@@ -286,7 +333,7 @@ public final class BinaryDictDecoderUtils {
static int readChildrenAddress(final DictBuffer dictBuffer,
final int optionFlags, final FormatOptions options) {
- if (options.mSupportsDynamicUpdate) {
+ if (options.supportsDynamicUpdate()) {
final int address = dictBuffer.readUnsignedInt24();
if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS;
if ((address & FormatSpec.MSB24) != 0) {
@@ -392,7 +439,7 @@ public final class BinaryDictDecoderUtils {
final FormatOptions options) {
dictDecoder.setPosition(headerSize);
final int count = dictDecoder.readPtNodeCount();
- int groupPos = headerSize + BinaryDictIOUtils.getPtNodeCountSize(count);
+ int groupPos = dictDecoder.getPosition();
final StringBuilder builder = new StringBuilder();
WeightedString result = null;
@@ -454,9 +501,9 @@ public final class BinaryDictDecoderUtils {
do { // Scan the linked-list node.
final int nodeArrayHeadPos = dictDecoder.getPosition();
final int count = dictDecoder.readPtNodeCount();
- int groupOffsetPos = nodeArrayHeadPos + BinaryDictIOUtils.getPtNodeCountSize(count);
+ int groupPos = dictDecoder.getPosition();
for (int i = count; i > 0; --i) { // Scan the array of PtNode.
- PtNodeInfo info = dictDecoder.readPtNode(groupOffsetPos, options);
+ PtNodeInfo info = dictDecoder.readPtNode(groupPos, options);
if (BinaryDictIOUtils.isMovedPtNode(info.mFlags, options)) continue;
ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets;
ArrayList<WeightedString> bigrams = null;
@@ -492,15 +539,15 @@ public final class BinaryDictDecoderUtils {
0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD),
0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED)));
}
- groupOffsetPos = info.mEndAddress;
+ groupPos = info.mEndAddress;
}
// reach the end of the array.
- if (options.mSupportsDynamicUpdate) {
+ if (options.supportsDynamicUpdate()) {
final boolean hasValidForwardLink = dictDecoder.readAndFollowForwardLink();
if (!hasValidForwardLink) break;
}
- } while (options.mSupportsDynamicUpdate && dictDecoder.hasNextPtNodeArray());
+ } while (options.supportsDynamicUpdate() && dictDecoder.hasNextPtNodeArray());
final PtNodeArray nodeArray = new PtNodeArray(nodeArrayContents);
nodeArray.mCachedAddressBeforeUpdate = nodeArrayOriginPos;
@@ -556,7 +603,7 @@ public final class BinaryDictDecoderUtils {
Map<Integer, PtNodeArray> reverseNodeArrayMapping = new TreeMap<Integer, PtNodeArray>();
Map<Integer, PtNode> reversePtNodeMapping = new TreeMap<Integer, PtNode>();
- final PtNodeArray root = readNodeArray(dictDecoder, fileHeader.mHeaderSize,
+ final PtNodeArray root = readNodeArray(dictDecoder, fileHeader.mBodyOffset,
reverseNodeArrayMapping, reversePtNodeMapping, fileHeader.mFormatOptions);
FusionDictionary newDict = new FusionDictionary(root, fileHeader.mDictionaryOptions);
@@ -592,32 +639,14 @@ public final class BinaryDictDecoderUtils {
/**
* Basic test to find out whether the file is a binary dictionary or not.
*
- * Concretely this only tests the magic number.
- *
* @param file The file to test.
* @return true if it's a binary dictionary, false otherwise
*/
public static boolean isBinaryDictionary(final File file) {
- FileInputStream inStream = null;
- try {
- inStream = new FileInputStream(file);
- final ByteBuffer buffer = inStream.getChannel().map(
- FileChannel.MapMode.READ_ONLY, 0, file.length());
- final int version = getFormatVersion(new ByteBufferDictBuffer(buffer));
- return (version >= FormatSpec.MINIMUM_SUPPORTED_VERSION
- && version <= FormatSpec.MAXIMUM_SUPPORTED_VERSION);
- } catch (FileNotFoundException e) {
+ final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file);
+ if (dictDecoder == null) {
return false;
- } catch (IOException e) {
- return false;
- } finally {
- if (inStream != null) {
- try {
- inStream.close();
- } catch (IOException e) {
- // do nothing
- }
- }
}
+ return dictDecoder.hasValidRawBinaryDictionary();
}
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
index f761829de..bb40e0dd5 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
@@ -16,10 +16,11 @@
package com.android.inputmethod.latin.makedict;
+import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding;
+import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
-import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
@@ -160,7 +161,7 @@ public class BinaryDictEncoderUtils {
node.mCachedSize = nodeSize;
size += nodeSize;
}
- if (options.mSupportsDynamicUpdate) {
+ if (options.supportsDynamicUpdate()) {
size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
}
ptNodeArray.mCachedSize = size;
@@ -245,6 +246,27 @@ public class BinaryDictEncoderUtils {
}
}
+ @UsedForTesting
+ static void writeUIntToDictBuffer(final DictBuffer dictBuffer, final int value,
+ final int size) {
+ switch(size) {
+ case 4:
+ dictBuffer.put((byte) ((value >> 24) & 0xFF));
+ /* fall through */
+ case 3:
+ dictBuffer.put((byte) ((value >> 16) & 0xFF));
+ /* fall through */
+ case 2:
+ dictBuffer.put((byte) ((value >> 8) & 0xFF));
+ /* fall through */
+ case 1:
+ dictBuffer.put((byte) (value & 0xFF));
+ break;
+ default:
+ /* nop */
+ }
+ }
+
// End utility methods
// This method is responsible for finding a nice ordering of the nodes that favors run-time
@@ -377,7 +399,7 @@ public class BinaryDictEncoderUtils {
nodeSize += FormatSpec.PTNODE_FREQUENCY_SIZE;
}
}
- if (formatOptions.mSupportsDynamicUpdate) {
+ if (formatOptions.supportsDynamicUpdate()) {
nodeSize += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
} else if (null != ptNode.mChildren) {
nodeSize += getByteSize(getOffsetToTargetNodeArrayDuringUpdate(ptNodeArray,
@@ -397,7 +419,7 @@ public class BinaryDictEncoderUtils {
ptNode.mCachedSize = nodeSize;
size += nodeSize;
}
- if (formatOptions.mSupportsDynamicUpdate) {
+ if (formatOptions.supportsDynamicUpdate()) {
size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
}
if (ptNodeArray.mCachedSize != size) {
@@ -513,7 +535,7 @@ public class BinaryDictEncoderUtils {
if (passes > MAX_PASSES) throw new RuntimeException("Too many passes - probably a bug");
} while (changesDone);
- if (formatOptions.mSupportsDynamicUpdate) {
+ if (formatOptions.supportsDynamicUpdate()) {
computeParentAddresses(flatNodes);
}
final PtNodeArray lastPtNodeArray = flatNodes.get(flatNodes.size() - 1);
@@ -622,7 +644,7 @@ public class BinaryDictEncoderUtils {
byte flags = 0;
if (hasMultipleChars) flags |= FormatSpec.FLAG_HAS_MULTIPLE_CHARS;
if (isTerminal) flags |= FormatSpec.FLAG_IS_TERMINAL;
- if (formatOptions.mSupportsDynamicUpdate) {
+ if (formatOptions.supportsDynamicUpdate()) {
flags |= FormatSpec.FLAG_IS_NOT_MOVED;
} else if (true) {
switch (childrenAddressSize) {
@@ -690,6 +712,13 @@ public class BinaryDictEncoderUtils {
+ word + " is " + unigramFrequency);
bigramFrequency = unigramFrequency;
}
+ bigramFlags += getBigramFrequencyDiff(unigramFrequency, bigramFrequency)
+ & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY;
+ return bigramFlags;
+ }
+
+ public static int getBigramFrequencyDiff(final int unigramFrequency,
+ final int bigramFrequency) {
// We compute the difference between 255 (which means probability = 1) and the
// unigram score. We split this into a number of discrete steps.
// Now, the steps are numbered 0~15; 0 represents an increase of 1 step while 15
@@ -723,22 +752,15 @@ public class BinaryDictEncoderUtils {
// include this bigram in the dictionary. For now, register as 0, and live with the
// small over-estimation that we get in this case. TODO: actually remove this bigram
// if discretizedFrequency < 0.
- final int finalBigramFrequency = discretizedFrequency > 0 ? discretizedFrequency : 0;
- bigramFlags += finalBigramFrequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY;
- return bigramFlags;
+ return discretizedFrequency > 0 ? discretizedFrequency : 0;
}
/**
- * Makes the 2-byte value for options flags.
+ * Makes the 2-byte value for options flags. Unused at the moment, and always 0.
*/
- private static final int makeOptionsValue(final FusionDictionary dictionary,
- final FormatOptions formatOptions) {
- final DictionaryOptions options = dictionary.mOptions;
- final boolean hasBigrams = dictionary.hasBigrams();
- return (options.mFrenchLigatureProcessing ? FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG : 0)
- + (options.mGermanUmlautProcessing ? FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG : 0)
- + (hasBigrams ? FormatSpec.CONTAINS_BIGRAMS_FLAG : 0)
- + (formatOptions.mSupportsDynamicUpdate ? FormatSpec.SUPPORTS_DYNAMIC_UPDATE : 0);
+ private static final int makeOptionsValue(final FormatOptions formatOptions) {
+ // TODO: why doesn't this handle CONTAINS_TIMESTAMP_FLAG?
+ return 0;
}
/**
@@ -826,7 +848,7 @@ public class BinaryDictEncoderUtils {
}
dictEncoder.writePtNode(ptNode, parentPosition, formatOptions, dict);
}
- if (formatOptions.mSupportsDynamicUpdate) {
+ if (formatOptions.supportsDynamicUpdate()) {
dictEncoder.writeForwardLinkAddress(FormatSpec.NO_FORWARD_LINK_ADDRESS);
}
if (dictEncoder.getPosition() != ptNodeArray.mCachedAddressAfterUpdate
@@ -927,7 +949,7 @@ public class BinaryDictEncoderUtils {
headerBuffer.write((byte) (0xFF & version));
// Options flags
- final int options = makeOptionsValue(dict, formatOptions);
+ final int options = makeOptionsValue(formatOptions);
headerBuffer.write((byte) (0xFF & (options >> 8)));
headerBuffer.write((byte) (0xFF & options));
final int headerSizeOffset = headerBuffer.size();
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
index d5516ef46..0dc50d14e 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
@@ -23,7 +23,6 @@ import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
import com.android.inputmethod.latin.utils.ByteArrayDictBuffer;
import java.io.File;
@@ -32,7 +31,6 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
-import java.util.Iterator;
import java.util.Map;
import java.util.Stack;
@@ -62,7 +60,7 @@ public final class BinaryDictIOUtils {
* Retrieves all node arrays without recursive call.
*/
private static void readUnigramsAndBigramsBinaryInner(final DictDecoder dictDecoder,
- final int headerSize, final Map<Integer, String> words,
+ final int bodyOffset, final Map<Integer, String> words,
final Map<Integer, Integer> frequencies,
final Map<Integer, ArrayList<PendingAttribute>> bigrams,
final FormatOptions formatOptions) {
@@ -71,7 +69,7 @@ public final class BinaryDictIOUtils {
Stack<Position> stack = new Stack<Position>();
int index = 0;
- Position initPos = new Position(headerSize, 0);
+ Position initPos = new Position(bodyOffset, 0);
stack.push(initPos);
while (!stack.empty()) {
@@ -87,7 +85,7 @@ public final class BinaryDictIOUtils {
if (p.mNumOfPtNode == Position.NOT_READ_PTNODE_COUNT) {
p.mNumOfPtNode = dictDecoder.readPtNodeCount();
- p.mAddress += getPtNodeCountSize(p.mNumOfPtNode);
+ p.mAddress = dictDecoder.getPosition();
p.mPosition = 0;
}
if (p.mNumOfPtNode == 0) {
@@ -112,7 +110,7 @@ public final class BinaryDictIOUtils {
}
if (p.mPosition == p.mNumOfPtNode) {
- if (formatOptions.mSupportsDynamicUpdate) {
+ if (formatOptions.supportsDynamicUpdate()) {
final boolean hasValidForwardLinkAddress =
dictDecoder.readAndFollowForwardLink();
if (hasValidForwardLinkAddress && dictDecoder.hasNextPtNodeArray()) {
@@ -154,7 +152,7 @@ public final class BinaryDictIOUtils {
UnsupportedFormatException {
// Read header
final FileHeader header = dictDecoder.readHeader();
- readUnigramsAndBigramsBinaryInner(dictDecoder, header.mHeaderSize, words,
+ readUnigramsAndBigramsBinaryInner(dictDecoder, header.mBodyOffset, words,
frequencies, bigrams, header.mFormatOptions);
}
@@ -228,7 +226,7 @@ public final class BinaryDictIOUtils {
// a forward link address that we need to consult and possibly resume
// search on the next node array in the linked list.
if (foundNextPtNode) break;
- if (!header.mFormatOptions.mSupportsDynamicUpdate) {
+ if (!header.mFormatOptions.supportsDynamicUpdate()) {
return FormatSpec.NOT_VALID_WORD;
}
@@ -245,8 +243,8 @@ public final class BinaryDictIOUtils {
/**
* @return the size written, in bytes. Always 3 bytes.
*/
- static int writeSInt24ToBuffer(final DictBuffer dictBuffer,
- final int value) {
+ @UsedForTesting
+ static int writeSInt24ToBuffer(final DictBuffer dictBuffer, final int value) {
final int absValue = Math.abs(value);
dictBuffer.put((byte)(((value < 0 ? 0x80 : 0) | (absValue >> 16)) & 0xFF));
dictBuffer.put((byte)((absValue >> 8) & 0xFF));
@@ -257,6 +255,7 @@ public final class BinaryDictIOUtils {
/**
* @return the size written, in bytes. Always 3 bytes.
*/
+ @UsedForTesting
static int writeSInt24ToStream(final OutputStream destination, final int value)
throws IOException {
final int absValue = Math.abs(value);
@@ -266,28 +265,7 @@ public final class BinaryDictIOUtils {
return 3;
}
- /**
- * @return the size written, in bytes. 1, 2, or 3 bytes.
- */
- private static int writeVariableAddress(final OutputStream destination, final int value)
- throws IOException {
- switch (BinaryDictEncoderUtils.getByteSize(value)) {
- case 1:
- destination.write((byte)value);
- break;
- case 2:
- destination.write((byte)(0xFF & (value >> 8)));
- destination.write((byte)(0xFF & value));
- break;
- case 3:
- destination.write((byte)(0xFF & (value >> 16)));
- destination.write((byte)(0xFF & (value >> 8)));
- destination.write((byte)(0xFF & value));
- break;
- }
- return BinaryDictEncoderUtils.getByteSize(value);
- }
-
+ @UsedForTesting
static void skipString(final DictBuffer dictBuffer,
final boolean hasMultipleChars) {
if (hasMultipleChars) {
@@ -301,176 +279,25 @@ public final class BinaryDictIOUtils {
}
/**
- * Write a string to a stream.
+ * Writes a PtNodeCount to the stream.
*
* @param destination the stream to write.
- * @param word the string to be written.
- * @return the size written, in bytes.
- * @throws IOException
+ * @param ptNodeCount the count.
+ * @return the size written in bytes.
*/
- private static int writeString(final OutputStream destination, final String word)
- throws IOException {
- int size = 0;
- final int length = word.length();
- for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
- final int codePoint = word.codePointAt(i);
- if (CharEncoding.getCharSize(codePoint) == 1) {
- destination.write((byte)codePoint);
- size++;
- } else {
- destination.write((byte)(0xFF & (codePoint >> 16)));
- destination.write((byte)(0xFF & (codePoint >> 8)));
- destination.write((byte)(0xFF & codePoint));
- size += 3;
- }
- }
- destination.write((byte)FormatSpec.PTNODE_CHARACTERS_TERMINATOR);
- size += FormatSpec.PTNODE_TERMINATOR_SIZE;
- return size;
- }
-
- /**
- * Write a PtNode to an output stream from a PtNodeInfo.
- * A PtNode is an in-memory representation of a node in the patricia trie.
- * A PtNode info is a container for low-level information about how the
- * PtNode is stored in the binary format.
- *
- * @param destination the stream to write.
- * @param info the PtNode info to be written.
- * @return the size written, in bytes.
- */
- private static int writePtNode(final OutputStream destination, final PtNodeInfo info)
- throws IOException {
- int size = FormatSpec.PTNODE_FLAGS_SIZE;
- destination.write((byte)info.mFlags);
- final int parentOffset = info.mParentAddress == FormatSpec.NO_PARENT_ADDRESS ?
- FormatSpec.NO_PARENT_ADDRESS : info.mParentAddress - info.mOriginalAddress;
- size += writeSInt24ToStream(destination, parentOffset);
-
- for (int i = 0; i < info.mCharacters.length; ++i) {
- if (CharEncoding.getCharSize(info.mCharacters[i]) == 1) {
- destination.write((byte)info.mCharacters[i]);
- size++;
- } else {
- size += writeSInt24ToStream(destination, info.mCharacters[i]);
- }
- }
- if (info.mCharacters.length > 1) {
- destination.write((byte)FormatSpec.PTNODE_CHARACTERS_TERMINATOR);
- size++;
- }
-
- if ((info.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0) {
- destination.write((byte)info.mFrequency);
- size++;
- }
-
- if (DBG) {
- MakedictLog.d("writePtNode origin=" + info.mOriginalAddress + ", size=" + size
- + ", child=" + info.mChildrenAddress + ", characters ="
- + new String(info.mCharacters, 0, info.mCharacters.length));
- }
- final int childrenOffset = info.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS ?
- 0 : info.mChildrenAddress - (info.mOriginalAddress + size);
- writeSInt24ToStream(destination, childrenOffset);
- size += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
-
- if (info.mShortcutTargets != null && info.mShortcutTargets.size() > 0) {
- final int shortcutListSize =
- BinaryDictEncoderUtils.getShortcutListSize(info.mShortcutTargets);
- destination.write((byte)(shortcutListSize >> 8));
- destination.write((byte)(shortcutListSize & 0xFF));
- size += 2;
- final Iterator<WeightedString> shortcutIterator = info.mShortcutTargets.iterator();
- while (shortcutIterator.hasNext()) {
- final WeightedString target = shortcutIterator.next();
- destination.write((byte)BinaryDictEncoderUtils.makeShortcutFlags(
- shortcutIterator.hasNext(), target.mFrequency));
- size++;
- size += writeString(destination, target.mWord);
- }
- }
-
- if (info.mBigrams != null) {
- // TODO: Consolidate this code with the code that computes the size of the bigram list
- // in BinaryDictEncoderUtils#computeActualNodeArraySize
- for (int i = 0; i < info.mBigrams.size(); ++i) {
-
- final int bigramFrequency = info.mBigrams.get(i).mFrequency;
- int bigramFlags = (i < info.mBigrams.size() - 1)
- ? FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT : 0;
- size++;
- final int bigramOffset = info.mBigrams.get(i).mAddress - (info.mOriginalAddress
- + size);
- bigramFlags |= (bigramOffset < 0) ? FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE : 0;
- switch (BinaryDictEncoderUtils.getByteSize(bigramOffset)) {
- case 1:
- bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE;
- break;
- case 2:
- bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES;
- break;
- case 3:
- bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES;
- break;
- }
- bigramFlags |= bigramFrequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY;
- destination.write((byte)bigramFlags);
- size += writeVariableAddress(destination, Math.abs(bigramOffset));
- }
- }
- return size;
- }
-
- /**
- * Compute the size of the PtNode.
- */
- static int computePtNodeSize(final PtNodeInfo info, final FormatOptions formatOptions) {
- int size = FormatSpec.PTNODE_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE
- + BinaryDictEncoderUtils.getPtNodeCharactersSize(info.mCharacters)
- + getChildrenAddressSize(info.mFlags, formatOptions);
- if ((info.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0) {
- size += FormatSpec.PTNODE_FREQUENCY_SIZE;
- }
- if (info.mShortcutTargets != null && !info.mShortcutTargets.isEmpty()) {
- size += BinaryDictEncoderUtils.getShortcutListSize(info.mShortcutTargets);
- }
- if (info.mBigrams != null) {
- for (final PendingAttribute attr : info.mBigrams) {
- size += FormatSpec.PTNODE_FLAGS_SIZE;
- size += BinaryDictEncoderUtils.getByteSize(attr.mAddress);
- }
- }
- return size;
- }
-
- /**
- * Write a node array to the stream.
- *
- * @param destination the stream to write.
- * @param infos an array of PtNodeInfo to be written.
- * @return the size written, in bytes.
- * @throws IOException
- */
- static int writeNodes(final OutputStream destination, final PtNodeInfo[] infos)
+ @UsedForTesting
+ static int writePtNodeCount(final OutputStream destination, final int ptNodeCount)
throws IOException {
- int size = getPtNodeCountSize(infos.length);
- switch (getPtNodeCountSize(infos.length)) {
- case 1:
- destination.write((byte)infos.length);
- break;
- case 2:
- final int encodedPtNodeCount =
- infos.length | FormatSpec.LARGE_PTNODE_ARRAY_SIZE_FIELD_SIZE_FLAG;
- destination.write((byte)(encodedPtNodeCount >> 8));
- destination.write((byte)(encodedPtNodeCount & 0xFF));
- break;
- default:
- throw new RuntimeException("Invalid node count size.");
+ final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount);
+ // the count must fit on one byte or two bytes.
+ // Please see comments in FormatSpec.
+ if (countSize != 1 && countSize != 2) {
+ throw new RuntimeException("Strange size from getPtNodeCountSize : " + countSize);
}
- for (final PtNodeInfo info : infos) size += writePtNode(destination, info);
- writeSInt24ToStream(destination, FormatSpec.NO_FORWARD_LINK_ADDRESS);
- return size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
+ final int encodedPtNodeCount = (countSize == 2) ?
+ (ptNodeCount | FormatSpec.LARGE_PTNODE_ARRAY_SIZE_FIELD_SIZE_FLAG) : ptNodeCount;
+ BinaryDictEncoderUtils.writeUIntToStream(destination, encodedPtNodeCount, countSize);
+ return countSize;
}
private static final int HEADER_READING_BUFFER_SIZE = 16384;
@@ -482,6 +309,7 @@ public final class BinaryDictIOUtils {
* @param file The file to read.
* @param offset The offset in the file where to start reading the data.
* @param length The length of the data file.
+ * @return the header of the specified dictionary file.
*/
private static FileHeader getDictionaryFileHeader(
final File file, final long offset, final long length)
@@ -503,6 +331,9 @@ public final class BinaryDictIOUtils {
}
}
);
+ if (dictDecoder == null) {
+ return null;
+ }
return dictDecoder.readHeader();
}
@@ -529,7 +360,7 @@ public final class BinaryDictIOUtils {
* Helper method to check whether the node is moved.
*/
public static boolean isMovedPtNode(final int flags, final FormatOptions options) {
- return options.mSupportsDynamicUpdate
+ return options.supportsDynamicUpdate()
&& ((flags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) == FormatSpec.FLAG_IS_MOVED);
}
@@ -538,14 +369,14 @@ public final class BinaryDictIOUtils {
*/
public static boolean supportsDynamicUpdate(final FormatOptions options) {
return options.mVersion >= FormatSpec.FIRST_VERSION_WITH_DYNAMIC_UPDATE
- && options.mSupportsDynamicUpdate;
+ && options.supportsDynamicUpdate();
}
/**
* Helper method to check whether the node is deleted.
*/
public static boolean isDeletedPtNode(final int flags, final FormatOptions formatOptions) {
- return formatOptions.mSupportsDynamicUpdate
+ return formatOptions.supportsDynamicUpdate()
&& ((flags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) == FormatSpec.FLAG_IS_DELETED);
}
@@ -568,7 +399,7 @@ public final class BinaryDictIOUtils {
static int getChildrenAddressSize(final int optionFlags,
final FormatOptions formatOptions) {
- if (formatOptions.mSupportsDynamicUpdate) return FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
+ if (formatOptions.supportsDynamicUpdate()) return FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) {
case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE:
return 1;
diff --git a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
index 3dbeee099..b4838f00f 100644
--- a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
@@ -35,6 +35,7 @@ import java.util.TreeMap;
/**
* An interface of binary dictionary decoders.
*/
+// TODO: Straighten out responsibility for the buffer's file pointer.
public interface DictDecoder {
/**
@@ -43,7 +44,7 @@ public interface DictDecoder {
public FileHeader readHeader() throws IOException, UnsupportedFormatException;
/**
- * Reads PtNode from nodeAddress.
+ * Reads PtNode from ptNodePos.
* @param ptNodePos the position of PtNode.
* @param formatOptions the format options.
* @return PtNodeInfo.
@@ -127,7 +128,8 @@ public interface DictDecoder {
* Opens the dictionary file and makes DictBuffer.
*/
@UsedForTesting
- public void openDictBuffer() throws FileNotFoundException, IOException;
+ public void openDictBuffer() throws FileNotFoundException, IOException,
+ UnsupportedFormatException;
@UsedForTesting
public boolean isDictBufferOpen();
@@ -228,4 +230,9 @@ public interface DictDecoder {
}
public void skipPtNode(final FormatOptions formatOptions);
+
+ /**
+ * @return whether this decoder has a valid binary dictionary that it can decode.
+ */
+ public boolean hasValidRawBinaryDictionary();
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java
deleted file mode 100644
index c4f7ec91f..000000000
--- a/java/src/com/android/inputmethod/latin/makedict/DictUpdater.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.makedict;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
-
-import java.io.IOException;
-import java.util.ArrayList;
-
-/**
- * An interface of a binary dictionary updater.
- */
-@UsedForTesting
-public interface DictUpdater extends DictDecoder {
-
- /**
- * Deletes the word from the binary dictionary.
- *
- * @param word the word to be deleted.
- */
- @UsedForTesting
- public void deleteWord(final String word) throws IOException, UnsupportedFormatException;
-
- /**
- * Inserts a word into a binary dictionary.
- *
- * @param word the word to be inserted.
- * @param frequency the frequency of the new word.
- * @param bigramStrings bigram list, or null if none.
- * @param shortcuts shortcut list, or null if none.
- * @param isBlackListEntry whether this should be a blacklist entry.
- */
- // TODO: Support batch insertion.
- @UsedForTesting
- public void insertWord(final String word, final int frequency,
- final ArrayList<WeightedString> bigramStrings,
- final ArrayList<WeightedString> shortcuts, final boolean isNotAWord,
- final boolean isBlackListEntry) throws IOException, UnsupportedFormatException;
-}
diff --git a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
deleted file mode 100644
index 28da9ffdd..000000000
--- a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
+++ /dev/null
@@ -1,492 +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.makedict;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
-import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
-import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
-import com.android.inputmethod.latin.utils.CollectionUtils;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-
-/**
- * The utility class to help dynamic updates on the binary dictionary.
- *
- * All the methods in this class are static.
- */
-@UsedForTesting
-public final class DynamicBinaryDictIOUtils {
- private static final boolean DBG = false;
- private static final int MAX_JUMPS = 10000;
-
- private DynamicBinaryDictIOUtils() {
- // This utility class is not publicly instantiable.
- }
-
- /* package */ static int markAsDeleted(final int flags) {
- return (flags & (~FormatSpec.MASK_CHILDREN_ADDRESS_TYPE)) | FormatSpec.FLAG_IS_DELETED;
- }
-
- /**
- * Update a parent address in a PtNode that is referred to by ptNodeOriginAddress.
- *
- * @param dictUpdater the DictUpdater to write.
- * @param ptNodeOriginAddress the address of the PtNode.
- * @param newParentAddress the absolute address of the parent.
- * @param formatOptions file format options.
- */
- private static void updateParentAddress(final Ver3DictUpdater dictUpdater,
- final int ptNodeOriginAddress, final int newParentAddress,
- final FormatOptions formatOptions) {
- final DictBuffer dictBuffer = dictUpdater.getDictBuffer();
- final int originalPosition = dictBuffer.position();
- dictBuffer.position(ptNodeOriginAddress);
- if (!formatOptions.mSupportsDynamicUpdate) {
- throw new RuntimeException("this file format does not support parent addresses");
- }
- final int flags = dictBuffer.readUnsignedByte();
- if (BinaryDictIOUtils.isMovedPtNode(flags, formatOptions)) {
- // If the node is moved, the parent address is stored in the destination node.
- // We are guaranteed to process the destination node later, so there is no need to
- // update anything here.
- dictBuffer.position(originalPosition);
- return;
- }
- if (DBG) {
- MakedictLog.d("update parent address flags=" + flags + ", " + ptNodeOriginAddress);
- }
- final int parentOffset = newParentAddress - ptNodeOriginAddress;
- BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, parentOffset);
- dictBuffer.position(originalPosition);
- }
-
- /**
- * Update parent addresses in a node array stored at ptNodeOriginAddress.
- *
- * @param dictUpdater the DictUpdater to be modified.
- * @param ptNodeOriginAddress the address of the node array to update.
- * @param newParentAddress the address to be written.
- * @param formatOptions file format options.
- */
- private static void updateParentAddresses(final Ver3DictUpdater dictUpdater,
- final int ptNodeOriginAddress, final int newParentAddress,
- final FormatOptions formatOptions) {
- final int originalPosition = dictUpdater.getPosition();
- dictUpdater.setPosition(ptNodeOriginAddress);
- do {
- final int count = dictUpdater.readPtNodeCount();
- for (int i = 0; i < count; ++i) {
- updateParentAddress(dictUpdater, dictUpdater.getPosition(), newParentAddress,
- formatOptions);
- dictUpdater.skipPtNode(formatOptions);
- }
- if (!dictUpdater.readAndFollowForwardLink()) break;
- if (dictUpdater.getPosition() == FormatSpec.NO_FORWARD_LINK_ADDRESS) break;
- } while (formatOptions.mSupportsDynamicUpdate);
- dictUpdater.setPosition(originalPosition);
- }
-
- /**
- * Update a children address in a PtNode that is addressed by ptNodeOriginAddress.
- *
- * @param dictUpdater the DictUpdater to write.
- * @param ptNodeOriginAddress the address of the PtNode.
- * @param newChildrenAddress the absolute address of the child.
- * @param formatOptions file format options.
- */
- private static void updateChildrenAddress(final Ver3DictUpdater dictUpdater,
- final int ptNodeOriginAddress, final int newChildrenAddress,
- final FormatOptions formatOptions) {
- final DictBuffer dictBuffer = dictUpdater.getDictBuffer();
- final int originalPosition = dictBuffer.position();
- dictBuffer.position(ptNodeOriginAddress);
- final int flags = dictBuffer.readUnsignedByte();
- BinaryDictDecoderUtils.readParentAddress(dictBuffer, formatOptions);
- BinaryDictIOUtils.skipString(dictBuffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
- if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) dictBuffer.readUnsignedByte();
- final int childrenOffset = newChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS
- ? FormatSpec.NO_CHILDREN_ADDRESS : newChildrenAddress - dictBuffer.position();
- BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, childrenOffset);
- dictBuffer.position(originalPosition);
- }
-
- /**
- * Helper method to move a PtNode to the tail of the file.
- */
- private static int movePtNode(final OutputStream destination,
- final Ver3DictUpdater dictUpdater, final PtNodeInfo info,
- final int nodeArrayOriginAddress, final int oldNodeAddress,
- final FormatOptions formatOptions) throws IOException {
- final DictBuffer dictBuffer = dictUpdater.getDictBuffer();
- updateParentAddress(dictUpdater, oldNodeAddress, dictBuffer.limit() + 1, formatOptions);
- dictBuffer.position(oldNodeAddress);
- final int currentFlags = dictBuffer.readUnsignedByte();
- dictBuffer.position(oldNodeAddress);
- dictBuffer.put((byte)(FormatSpec.FLAG_IS_MOVED | (currentFlags
- & (~FormatSpec.MASK_MOVE_AND_DELETE_FLAG))));
- int size = FormatSpec.PTNODE_FLAGS_SIZE;
- updateForwardLink(dictUpdater, nodeArrayOriginAddress, dictBuffer.limit(), formatOptions);
- size += BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { info });
- return size;
- }
-
- @SuppressWarnings("unused")
- private static void updateForwardLink(final Ver3DictUpdater dictUpdater,
- final int nodeArrayOriginAddress, final int newNodeArrayAddress,
- final FormatOptions formatOptions) {
- final DictBuffer dictBuffer = dictUpdater.getDictBuffer();
- dictUpdater.setPosition(nodeArrayOriginAddress);
- int jumpCount = 0;
- while (jumpCount++ < MAX_JUMPS) {
- final int count = dictUpdater.readPtNodeCount();
- for (int i = 0; i < count; ++i) {
- dictUpdater.readPtNode(dictUpdater.getPosition(), formatOptions);
- }
- final int forwardLinkAddress = dictBuffer.readUnsignedInt24();
- if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
- dictBuffer.position(dictBuffer.position() - FormatSpec.FORWARD_LINK_ADDRESS_SIZE);
- BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, newNodeArrayAddress);
- return;
- }
- dictBuffer.position(forwardLinkAddress);
- }
- if (DBG && jumpCount >= MAX_JUMPS) {
- throw new RuntimeException("too many jumps, probably a bug.");
- }
- }
-
- /**
- * Move a PtNode that is referred to by oldPtNodeOrigin to the tail of the file, and set the
- * children address to the byte after the PtNode.
- *
- * @param fileEndAddress the address of the tail of the file.
- * @param codePoints the characters to put inside the PtNode.
- * @param length how many code points to read from codePoints.
- * @param flags the flags for this PtNode.
- * @param frequency the frequency of this terminal.
- * @param parentAddress the address of the parent PtNode of this PtNode.
- * @param shortcutTargets the shortcut targets for this PtNode.
- * @param bigrams the bigrams for this PtNode.
- * @param destination the stream representing the tail of the file.
- * @param dictUpdater the DictUpdater.
- * @param oldPtNodeArrayOrigin the origin of the old PtNode array this PtNode was a part of.
- * @param oldPtNodeOrigin the old origin where this PtNode used to be stored.
- * @param formatOptions format options for this dictionary.
- * @return the size written, in bytes.
- * @throws IOException if the file can't be accessed
- */
- private static int movePtNode(final int fileEndAddress, final int[] codePoints,
- final int length, final int flags, final int frequency, final int parentAddress,
- final ArrayList<WeightedString> shortcutTargets,
- final ArrayList<PendingAttribute> bigrams, final OutputStream destination,
- final Ver3DictUpdater dictUpdater, final int oldPtNodeArrayOrigin,
- final int oldPtNodeOrigin, final FormatOptions formatOptions) throws IOException {
- int size = 0;
- final int newPtNodeOrigin = fileEndAddress + 1;
- final int[] writtenCharacters = Arrays.copyOfRange(codePoints, 0, length);
- final PtNodeInfo tmpInfo = new PtNodeInfo(newPtNodeOrigin, -1 /* endAddress */,
- flags, writtenCharacters, frequency, parentAddress, FormatSpec.NO_CHILDREN_ADDRESS,
- shortcutTargets, bigrams);
- size = BinaryDictIOUtils.computePtNodeSize(tmpInfo, formatOptions);
- final PtNodeInfo newInfo = new PtNodeInfo(newPtNodeOrigin, newPtNodeOrigin + size,
- flags, writtenCharacters, frequency, parentAddress,
- fileEndAddress + 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE, shortcutTargets,
- bigrams);
- movePtNode(destination, dictUpdater, newInfo, oldPtNodeArrayOrigin, oldPtNodeOrigin,
- formatOptions);
- return 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
- }
-
- /**
- * Converts a list of WeightedString to a list of PendingAttribute.
- */
- public static ArrayList<PendingAttribute> resolveBigramPositions(final DictUpdater dictUpdater,
- final ArrayList<WeightedString> bigramStrings)
- throws IOException, UnsupportedFormatException {
- if (bigramStrings == null) return CollectionUtils.newArrayList();
- final ArrayList<PendingAttribute> bigrams = CollectionUtils.newArrayList();
- for (final WeightedString bigram : bigramStrings) {
- final int pos = dictUpdater.getTerminalPosition(bigram.mWord);
- if (pos == FormatSpec.NOT_VALID_WORD) {
- // TODO: figure out what is the correct thing to do here.
- } else {
- bigrams.add(new PendingAttribute(bigram.mFrequency, pos));
- }
- }
- return bigrams;
- }
-
- /**
- * Insert a word into a binary dictionary.
- *
- * @param dictUpdater the dict updater.
- * @param destination a stream to the underlying file, with the pointer at the end of the file.
- * @param word the word to insert.
- * @param frequency the frequency of the new word.
- * @param bigramStrings bigram list, or null if none.
- * @param shortcuts shortcut list, or null if none.
- * @param isBlackListEntry whether this should be a blacklist entry.
- * @throws IOException if the file can't be accessed.
- * @throws UnsupportedFormatException if the existing dictionary is in an unexpected format.
- */
- // TODO: Support batch insertion.
- // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary.
- @UsedForTesting
- public static void insertWord(final Ver3DictUpdater dictUpdater,
- final OutputStream destination, final String word, final int frequency,
- final ArrayList<WeightedString> bigramStrings,
- final ArrayList<WeightedString> shortcuts, final boolean isNotAWord,
- final boolean isBlackListEntry)
- throws IOException, UnsupportedFormatException {
- final ArrayList<PendingAttribute> bigrams = resolveBigramPositions(dictUpdater,
- bigramStrings);
- final DictBuffer dictBuffer = dictUpdater.getDictBuffer();
-
- final boolean isTerminal = true;
- final boolean hasBigrams = !bigrams.isEmpty();
- final boolean hasShortcuts = shortcuts != null && !shortcuts.isEmpty();
-
- // find the insert position of the word.
- if (dictBuffer.position() != 0) dictBuffer.position(0);
- final FileHeader fileHeader = dictUpdater.readHeader();
-
- int wordPos = 0, address = dictBuffer.position(), nodeOriginAddress = dictBuffer.position();
- final int[] codePoints = FusionDictionary.getCodePoints(word);
- final int wordLen = codePoints.length;
-
- for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) {
- if (wordPos >= wordLen) break;
- nodeOriginAddress = dictBuffer.position();
- int nodeParentAddress = -1;
- final int ptNodeCount = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
- boolean foundNextNode = false;
-
- for (int i = 0; i < ptNodeCount; ++i) {
- address = dictBuffer.position();
- final PtNodeInfo currentInfo = dictUpdater.readPtNode(address,
- fileHeader.mFormatOptions);
- final boolean isMovedNode = BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags,
- fileHeader.mFormatOptions);
- if (isMovedNode) continue;
- nodeParentAddress = (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS)
- ? FormatSpec.NO_PARENT_ADDRESS : currentInfo.mParentAddress + address;
- boolean matched = true;
- for (int p = 0; p < currentInfo.mCharacters.length; ++p) {
- if (wordPos + p >= wordLen) {
- /*
- * splitting
- * before
- * abcd - ef
- *
- * insert "abc"
- *
- * after
- * abc - d - ef
- */
- final int newNodeAddress = dictBuffer.limit();
- final int flags = BinaryDictEncoderUtils.makePtNodeFlags(p > 1,
- isTerminal, 0, hasShortcuts, hasBigrams, false /* isNotAWord */,
- false /* isBlackListEntry */, fileHeader.mFormatOptions);
- int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p, flags,
- frequency, nodeParentAddress, shortcuts, bigrams, destination,
- dictUpdater, nodeOriginAddress, address, fileHeader.mFormatOptions);
-
- final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p,
- currentInfo.mCharacters.length);
- if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
- updateParentAddresses(dictUpdater, currentInfo.mChildrenAddress,
- newNodeAddress + written + 1, fileHeader.mFormatOptions);
- }
- final PtNodeInfo newInfo2 = new PtNodeInfo(
- newNodeAddress + written + 1, -1 /* endAddress */,
- currentInfo.mFlags, characters2, currentInfo.mFrequency,
- newNodeAddress + 1, currentInfo.mChildrenAddress,
- currentInfo.mShortcutTargets, currentInfo.mBigrams);
- BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { newInfo2 });
- return;
- } else if (codePoints[wordPos + p] != currentInfo.mCharacters[p]) {
- if (p > 0) {
- /*
- * splitting
- * before
- * ab - cd
- *
- * insert "ac"
- *
- * after
- * a - b - cd
- * |
- * - c
- */
-
- final int newNodeAddress = dictBuffer.limit();
- final int childrenAddress = currentInfo.mChildrenAddress;
-
- // move prefix
- final int prefixFlags = BinaryDictEncoderUtils.makePtNodeFlags(p > 1,
- false /* isTerminal */, 0 /* childrenAddressSize*/,
- false /* hasShortcut */, false /* hasBigrams */,
- false /* isNotAWord */, false /* isBlackListEntry */,
- fileHeader.mFormatOptions);
- int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p,
- prefixFlags, -1 /* frequency */, nodeParentAddress, null, null,
- destination, dictUpdater, nodeOriginAddress, address,
- fileHeader.mFormatOptions);
-
- final int[] suffixCharacters = Arrays.copyOfRange(
- currentInfo.mCharacters, p, currentInfo.mCharacters.length);
- if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
- updateParentAddresses(dictUpdater, currentInfo.mChildrenAddress,
- newNodeAddress + written + 1, fileHeader.mFormatOptions);
- }
- final int suffixFlags = BinaryDictEncoderUtils.makePtNodeFlags(
- suffixCharacters.length > 1,
- (currentInfo.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0,
- 0 /* childrenAddressSize */,
- (currentInfo.mFlags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)
- != 0,
- (currentInfo.mFlags & FormatSpec.FLAG_HAS_BIGRAMS) != 0,
- isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
- final PtNodeInfo suffixInfo = new PtNodeInfo(
- newNodeAddress + written + 1, -1 /* endAddress */, suffixFlags,
- suffixCharacters, currentInfo.mFrequency, newNodeAddress + 1,
- currentInfo.mChildrenAddress, currentInfo.mShortcutTargets,
- currentInfo.mBigrams);
- written += BinaryDictIOUtils.computePtNodeSize(suffixInfo,
- fileHeader.mFormatOptions) + 1;
-
- final int[] newCharacters = Arrays.copyOfRange(codePoints, wordPos + p,
- codePoints.length);
- final int flags = BinaryDictEncoderUtils.makePtNodeFlags(
- newCharacters.length > 1, isTerminal,
- 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
- final PtNodeInfo newInfo = new PtNodeInfo(
- newNodeAddress + written, -1 /* endAddress */, flags,
- newCharacters, frequency, newNodeAddress + 1,
- FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
- BinaryDictIOUtils.writeNodes(destination,
- new PtNodeInfo[] { suffixInfo, newInfo });
- return;
- }
- matched = false;
- break;
- }
- }
-
- if (matched) {
- if (wordPos + currentInfo.mCharacters.length == wordLen) {
- // the word exists in the dictionary.
- // only update the PtNode.
- final int newNodeAddress = dictBuffer.limit();
- final boolean hasMultipleChars = currentInfo.mCharacters.length > 1;
- final int flags = BinaryDictEncoderUtils.makePtNodeFlags(hasMultipleChars,
- isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
- final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress + 1,
- -1 /* endAddress */, flags, currentInfo.mCharacters, frequency,
- nodeParentAddress, currentInfo.mChildrenAddress, shortcuts,
- bigrams);
- movePtNode(destination, dictUpdater, newInfo, nodeOriginAddress, address,
- fileHeader.mFormatOptions);
- return;
- }
- wordPos += currentInfo.mCharacters.length;
- if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) {
- /*
- * found the prefix of the word.
- * make new PtNode and link to the PtNode from this PtNode.
- *
- * before
- * ab - cd
- *
- * insert "abcde"
- *
- * after
- * ab - cd - e
- */
- final int newNodeArrayAddress = dictBuffer.limit();
- updateChildrenAddress(dictUpdater, address, newNodeArrayAddress,
- fileHeader.mFormatOptions);
- final int newNodeAddress = newNodeArrayAddress + 1;
- final boolean hasMultipleChars = (wordLen - wordPos) > 1;
- final int flags = BinaryDictEncoderUtils.makePtNodeFlags(hasMultipleChars,
- isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
- final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
- final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress, -1, flags,
- characters, frequency, address, FormatSpec.NO_CHILDREN_ADDRESS,
- shortcuts, bigrams);
- BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { newInfo });
- return;
- }
- dictBuffer.position(currentInfo.mChildrenAddress);
- foundNextNode = true;
- break;
- }
- }
-
- if (foundNextNode) continue;
-
- // reached the end of the array.
- final int linkAddressPosition = dictBuffer.position();
- int nextLink = dictBuffer.readUnsignedInt24();
- if ((nextLink & FormatSpec.MSB24) != 0) {
- nextLink = -(nextLink & FormatSpec.SINT24_MAX);
- }
- if (nextLink == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
- /*
- * expand this node.
- *
- * before
- * ab - cd
- *
- * insert "abef"
- *
- * after
- * ab - cd
- * |
- * - ef
- */
-
- // change the forward link address.
- final int newNodeAddress = dictBuffer.limit();
- dictBuffer.position(linkAddressPosition);
- BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, newNodeAddress);
-
- final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
- final int flags = BinaryDictEncoderUtils.makePtNodeFlags(characters.length > 1,
- isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
- final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress + 1,
- -1 /* endAddress */, flags, characters, frequency, nodeParentAddress,
- FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
- BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[]{ newInfo });
- return;
- } else {
- depth--;
- dictBuffer.position(nextLink);
- }
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
index b56234f6d..437fa942b 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -40,12 +40,8 @@ public final class FormatSpec {
* 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
- * o | has bigrams ? 1 bit, 1 = yes, 0 = no : CONTAINS_BIGRAMS_FLAG
- * n | FRENCH_LIGATURE_PROCESSING_FLAG
- * f | supports dynamic updates ? 1 bit, 1 = yes, 0 = no : SUPPORTS_DYNAMIC_UPDATE
- * l | GERMAN_UMLAUT_PROCESSING_FLAG
- * a |
- * gs
+ * o |
+ * nflags
*
* h |
* e | size of the file header, 4bytes
@@ -82,45 +78,36 @@ public final class FormatSpec {
* s
*
* f |
- * o | IF SUPPORTS_DYNAMIC_UPDATE (defined in the file header)
- * r | forward link address, 3byte
- * w | 1 byte = bbbbbbbb match
- * a | case 1xxxxxxx => -((xxxxxxx << 16) + (next byte << 8) + next byte)
- * r | otherwise => (xxxxxxx << 16) + (next byte << 8) + next byte
- * d |
- * linkaddress
+ * o | forward link address, 3byte
+ * r | 1 byte = bbbbbbbb match
+ * w | case 1xxxxxxx => -((xxxxxxx << 16) + (next byte << 8) + next byte)
+ * a | otherwise => (xxxxxxx << 16) + (next byte << 8) + next byte
+ * r |
+ * dlinkaddress
*/
/* Node (FusionDictionary.PtNode) layout is as follows:
- * | IF !SUPPORTS_DYNAMIC_UPDATE
- * | addressType xx : mask with MASK_CHILDREN_ADDRESS_TYPE
- * | 2 bits, 00 = no children : FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS
- * f | 01 = 1 byte : FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE
- * l | 10 = 2 bytes : FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES
- * a | 11 = 3 bytes : FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES
- * g | ELSE
- * s | 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
- * | the new address is stored in the same place as the parent address
- * | is deleted? 10 = yes : FLAG_IS_DELETED
- * | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS
- * | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL
- * | has shortcut targets ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_SHORTCUT_TARGETS
+ * | 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
+ * 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 | IF SUPPORTS_DYNAMIC_UPDATE (defined in the file header)
- * r | parent address, 3byte
- * e | 1 byte = bbbbbbbb match
- * n | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte)
- * t | otherwise => (bbbbbbbb << 16) + (next byte << 8) + next byte
- * a | This address is relative to the head of the PtNode.
- * d | If the node doesn't have a parent, this field is set to 0.
+ * 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 |
- * ress
+ * dress
*
* c | IF FLAG_HAS_MULTIPLE_CHARS
* h | char, char, char, char n * (1 or 3 bytes) : use PtNodeInfo for i/o helpers
@@ -134,23 +121,16 @@ public final class FormatSpec {
* e | frequency 1 byte
* q |
*
- * c | IF SUPPORTS_DYNAMIC_UPDATE
- * 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 | ELSIF 00 = FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS == addressType
- * a | // nothing
- * d | ELSIF 01 = FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE == addressType
- * d | children address, 1 byte
- * r | ELSIF 10 = FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES == addressType
- * e | children address, 2 bytes
- * s | ELSE // 11 = FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES = addressType
- * s | children address, 3 bytes
- * | END
- * | This address is relative to the position of this field.
+ * 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
*
* | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS
* | shortcut string list
@@ -199,20 +179,21 @@ public final class FormatSpec {
*/
public static final int MAGIC_NUMBER = 0x9BC13AFE;
- static final int MINIMUM_SUPPORTED_VERSION = 2;
- static final int MAXIMUM_SUPPORTED_VERSION = 4;
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;
- static final int VERSION3 = 3;
- static final int VERSION4 = 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
+ // us to change the format during development while having testing devices remove
+ // older files with each upgrade, while still having a readable versioning scheme.
+ public static final int VERSION2 = 2;
+ public static final int VERSION4 = 401;
+ static final int MINIMUM_SUPPORTED_VERSION = VERSION2;
+ static final int MAXIMUM_SUPPORTED_VERSION = VERSION4;
// These options need to be the same numeric values as the one in the native reading code.
- static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1;
// TODO: Make the native reading code read this variable.
- static final int SUPPORTS_DYNAMIC_UPDATE = 0x2;
- static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4;
- static final int CONTAINS_BIGRAMS_FLAG = 0x8;
static final int CONTAINS_TIMESTAMP_FLAG = 0x10;
// TODO: Make this value adaptative to content data, store it in the header, and
@@ -263,8 +244,10 @@ public final class FormatSpec {
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.
+ // 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";
static final String UNIGRAM_TIMESTAMP_FILE_EXTENSION = ".timestamp";
// tat = Terminal Address Table
@@ -278,9 +261,9 @@ public final class FormatSpec {
static final int UNIGRAM_TIMESTAMP_SIZE = 4;
// With the English main dictionary as of October 2013, the size of bigram address table is
- // is 584KB with the block size being 4.
- // This is 91% of that of full address table.
- static final int BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 4;
+ // 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 = 2;
static final int BIGRAM_FREQ_CONTENT_INDEX = 0;
static final int BIGRAM_TIMESTAMP_CONTENT_INDEX = 1;
@@ -293,7 +276,7 @@ public final class FormatSpec {
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
- // 29KB with the block size being 64.
+ // 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";
@@ -331,43 +314,36 @@ public final class FormatSpec {
*/
public static final class FormatOptions {
public final int mVersion;
- public final boolean mSupportsDynamicUpdate;
public final boolean mHasTerminalId;
public final boolean mHasTimestamp;
- @UsedForTesting
- public FormatOptions(final int version) {
- this(version, false);
- }
@UsedForTesting
- public FormatOptions(final int version, final boolean supportsDynamicUpdate) {
- this(version, supportsDynamicUpdate, false /* hasTimestamp */);
+ public FormatOptions(final int version) {
+ this(version, false /* hasTimestamp */);
}
- public FormatOptions(final int version, final boolean supportsDynamicUpdate,
- final boolean hasTimestamp) {
+ public FormatOptions(final int version, final boolean hasTimestamp) {
mVersion = version;
- if (version < FIRST_VERSION_WITH_DYNAMIC_UPDATE && supportsDynamicUpdate) {
- throw new RuntimeException("Dynamic updates are only supported with versions "
- + FIRST_VERSION_WITH_DYNAMIC_UPDATE + " and ulterior.");
- }
- mSupportsDynamicUpdate = supportsDynamicUpdate;
mHasTerminalId = (version >= FIRST_VERSION_WITH_TERMINAL_ID);
mHasTimestamp = hasTimestamp;
}
+
+ public boolean supportsDynamicUpdate() {
+ return mVersion >= FIRST_VERSION_WITH_DYNAMIC_UPDATE;
+ }
}
/**
* Class representing file header.
*/
public static final class FileHeader {
- public final int mHeaderSize;
+ public final int mBodyOffset;
public final DictionaryOptions mDictionaryOptions;
public final FormatOptions mFormatOptions;
// Note that these are corresponding definitions in native code in latinime::HeaderPolicy
// and latinime::HeaderReadWriteUtils.
- public static final String SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE = "SUPPORTS_DYNAMIC_UPDATE";
public static final String USES_FORGETTING_CURVE_ATTRIBUTE = "USES_FORGETTING_CURVE";
+ public static final String HAS_HISTORICAL_INFO_ATTRIBUTE = "HAS_HISTORICAL_INFO";
public static final String ATTRIBUTE_VALUE_TRUE = "1";
public static final String DICTIONARY_VERSION_ATTRIBUTE = "version";
@@ -375,10 +351,20 @@ public final class FormatSpec {
public static final String DICTIONARY_ID_ATTRIBUTE = "dictionary";
private static final String DICTIONARY_DESCRIPTION_ATTRIBUTE = "description";
public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions,
- final FormatOptions formatOptions) {
- mHeaderSize = headerSize;
+ final FormatOptions formatOptions) throws UnsupportedFormatException {
mDictionaryOptions = dictionaryOptions;
mFormatOptions = formatOptions;
+ mBodyOffset = formatOptions.mVersion < VERSION4 ? headerSize : 0;
+ if (null == getLocaleString()) {
+ throw new UnsupportedFormatException("Cannot create a FileHeader without a locale");
+ }
+ if (null == getVersion()) {
+ throw new UnsupportedFormatException(
+ "Cannot create a FileHeader without a version");
+ }
+ if (null == getId()) {
+ throw new UnsupportedFormatException("Cannot create a FileHeader without an ID");
+ }
}
// Helper method to get the locale as a String
@@ -415,7 +401,7 @@ public final class FormatSpec {
if (dictFile.isDirectory()) {
return new Ver4DictDecoder(dictFile, bufferType);
} else if (dictFile.isFile()) {
- return new Ver3DictDecoder(dictFile, bufferType);
+ return new Ver2DictDecoder(dictFile, bufferType);
}
return null;
}
@@ -425,7 +411,7 @@ public final class FormatSpec {
if (dictFile.isDirectory()) {
return new Ver4DictDecoder(dictFile, factory);
} else if (dictFile.isFile()) {
- return new Ver3DictDecoder(dictFile, factory);
+ return new Ver2DictDecoder(dictFile, factory);
}
return null;
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index 3bb218bea..fdf2ae7b5 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -303,14 +303,9 @@ public final class FusionDictionary implements Iterable<Word> {
* Options global to the dictionary.
*/
public static final class DictionaryOptions {
- public final boolean mGermanUmlautProcessing;
- public final boolean mFrenchLigatureProcessing;
public final HashMap<String, String> mAttributes;
- public DictionaryOptions(final HashMap<String, String> attributes,
- final boolean germanUmlautProcessing, final boolean frenchLigatureProcessing) {
+ public DictionaryOptions(final HashMap<String, String> attributes) {
mAttributes = attributes;
- mGermanUmlautProcessing = germanUmlautProcessing;
- mFrenchLigatureProcessing = frenchLigatureProcessing;
}
@Override
public String toString() { // Convenience method
@@ -339,14 +334,6 @@ public final class FusionDictionary implements Iterable<Word> {
}
s.append("\n");
}
- if (mGermanUmlautProcessing) {
- s.append(indent);
- s.append("Needs German umlaut processing\n");
- }
- if (mFrenchLigatureProcessing) {
- s.append(indent);
- s.append("Needs French ligature processing\n");
- }
return s.toString();
}
}
@@ -701,138 +688,6 @@ public final class FusionDictionary implements Iterable<Word> {
}
/**
- * Recursively count the number of nodes in a given branch of the trie.
- *
- * @param nodeArray the node array to count.
- * @return the number of nodes in this branch.
- */
- public static int countNodeArrays(final PtNodeArray nodeArray) {
- int size = 1;
- for (int i = nodeArray.mData.size() - 1; i >= 0; --i) {
- PtNode ptNode = nodeArray.mData.get(i);
- if (null != ptNode.mChildren)
- size += countNodeArrays(ptNode.mChildren);
- }
- return size;
- }
-
- // Recursively find out whether there are any bigrams.
- // This can be pretty expensive especially if there aren't any (we return as soon
- // as we find one, so it's much cheaper if there are bigrams)
- private static boolean hasBigramsInternal(final PtNodeArray nodeArray) {
- if (null == nodeArray) return false;
- for (int i = nodeArray.mData.size() - 1; i >= 0; --i) {
- PtNode ptNode = nodeArray.mData.get(i);
- if (null != ptNode.mBigrams) return true;
- if (hasBigramsInternal(ptNode.mChildren)) return true;
- }
- return false;
- }
-
- /**
- * Finds out whether there are any bigrams in this dictionary.
- *
- * @return true if there is any bigram, false otherwise.
- */
- // TODO: this is expensive especially for large dictionaries without any bigram.
- // The up side is, this is always accurate and correct and uses no memory. We should
- // find a more efficient way of doing this, without compromising too much on memory
- // and ease of use.
- public boolean hasBigrams() {
- return hasBigramsInternal(mRootNodeArray);
- }
-
- // Historically, the tails of the words were going to be merged to save space.
- // However, that would prevent the code to search for a specific address in log(n)
- // time so this was abandoned.
- // The code is still of interest as it does add some compression to any dictionary
- // that has no need for attributes. Implementations that does not read attributes should be
- // able to read a dictionary with merged tails.
- // Also, the following code does support frequencies, as in, it will only merges
- // tails that share the same frequency. Though it would result in the above loss of
- // performance while searching by address, it is still technically possible to merge
- // tails that contain attributes, but this code does not take that into account - it does
- // not compare attributes and will merge terminals with different attributes regardless.
- public void mergeTails() {
- MakedictLog.i("Do not merge tails");
- return;
-
-// MakedictLog.i("Merging PtNodes. Number of PtNodes : " + countPtNodes(root));
-// MakedictLog.i("Number of PtNodes : " + countPtNodes(root));
-//
-// final HashMap<String, ArrayList<PtNodeArray>> repository =
-// new HashMap<String, ArrayList<PtNodeArray>>();
-// mergeTailsInner(repository, root);
-//
-// MakedictLog.i("Number of different pseudohashes : " + repository.size());
-// int size = 0;
-// for (ArrayList<PtNodeArray> a : repository.values()) {
-// size += a.size();
-// }
-// MakedictLog.i("Number of nodes after merge : " + (1 + size));
-// MakedictLog.i("Recursively seen nodes : " + countNodes(root));
- }
-
- // The following methods are used by the deactivated mergeTails()
-// private static boolean isEqual(PtNodeArray a, PtNodeArray b) {
-// if (null == a && null == b) return true;
-// if (null == a || null == b) return false;
-// if (a.data.size() != b.data.size()) return false;
-// final int size = a.data.size();
-// for (int i = size - 1; i >= 0; --i) {
-// PtNode aPtNode = a.data.get(i);
-// PtNode bPtNode = b.data.get(i);
-// if (aPtNode.frequency != bPtNode.frequency) return false;
-// if (aPtNode.alternates == null && bPtNode.alternates != null) return false;
-// if (aPtNode.alternates != null && !aPtNode.equals(bPtNode.alternates)) return false;
-// if (!Arrays.equals(aPtNode.chars, bPtNode.chars)) return false;
-// if (!isEqual(aPtNode.children, bPtNode.children)) return false;
-// }
-// return true;
-// }
-
-// static private HashMap<String, ArrayList<PtNodeArray>> mergeTailsInner(
-// final HashMap<String, ArrayList<PtNodeArray>> map, final PtNodeArray nodeArray) {
-// final ArrayList<PtNode> branches = nodeArray.data;
-// final int nodeSize = branches.size();
-// for (int i = 0; i < nodeSize; ++i) {
-// PtNode ptNode = branches.get(i);
-// if (null != ptNode.children) {
-// String pseudoHash = getPseudoHash(ptNode.children);
-// ArrayList<PtNodeArray> similarList = map.get(pseudoHash);
-// if (null == similarList) {
-// similarList = new ArrayList<PtNodeArray>();
-// map.put(pseudoHash, similarList);
-// }
-// boolean merged = false;
-// for (PtNodeArray similar : similarList) {
-// if (isEqual(ptNode.children, similar)) {
-// ptNode.children = similar;
-// merged = true;
-// break;
-// }
-// }
-// if (!merged) {
-// similarList.add(ptNode.children);
-// }
-// mergeTailsInner(map, ptNode.children);
-// }
-// }
-// return map;
-// }
-
-// private static String getPseudoHash(final PtNodeArray nodeArray) {
-// StringBuilder s = new StringBuilder();
-// for (PtNode ptNode : nodeArray.data) {
-// s.append(ptNode.frequency);
-// for (int ch : ptNode.chars) {
-// s.append(Character.toChars(ch));
-// }
-// }
-// return s.toString();
-// }
-
- /**
* Iterator to walk through a dictionary.
*
* This is purely for convenience.
diff --git a/java/src/com/android/inputmethod/latin/makedict/SparseTableContentReader.java b/java/src/com/android/inputmethod/latin/makedict/SparseTableContentReader.java
new file mode 100644
index 000000000..63e1f56f5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/SparseTableContentReader.java
@@ -0,0 +1,126 @@
+/*
+ * 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.makedict;
+
+import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
+import com.android.inputmethod.latin.makedict.DictDecoder.DictionaryBufferFactory;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+/**
+ * An auxiliary class for reading SparseTable and data written by SparseTableContentWriter.
+ */
+public class SparseTableContentReader {
+
+ /**
+ * An interface of a function which is passed to SparseTableContentReader.read.
+ */
+ public interface SparseTableContentReaderInterface {
+ /**
+ * Reads data.
+ *
+ * @param buffer the DictBuffer. The position of the buffer is set to the head of data.
+ */
+ public void read(final DictBuffer buffer);
+ }
+
+ protected final int mContentCount;
+ protected final int mBlockSize;
+ protected final File mBaseDir;
+ protected final File mLookupTableFile;
+ protected final File[] mAddressTableFiles;
+ protected final File[] mContentFiles;
+ protected DictBuffer mLookupTableBuffer;
+ protected final DictBuffer[] mAddressTableBuffers;
+ private final DictBuffer[] mContentBuffers;
+ protected final DictionaryBufferFactory mFactory;
+
+ /**
+ * Sole constructor of SparseTableContentReader.
+ *
+ * @param name the name of SparseTable.
+ * @param blockSize the block size of the content table.
+ * @param baseDir the directory which contains the files of the content table.
+ * @param contentFilenames the file names of content files.
+ * @param contentSuffixes the ids of contents. These ids are used for a suffix of a name of
+ * address files and content files.
+ * @param factory the DictionaryBufferFactory which is used for opening the files.
+ */
+ public SparseTableContentReader(final String name, final int blockSize, final File baseDir,
+ final String[] contentFilenames, final String[] contentSuffixes,
+ final DictionaryBufferFactory factory) {
+ if (contentFilenames.length != contentSuffixes.length) {
+ throw new RuntimeException("The length of contentFilenames and the length of"
+ + " contentSuffixes are different " + contentFilenames.length + ", "
+ + contentSuffixes.length);
+ }
+ mBlockSize = blockSize;
+ mBaseDir = baseDir;
+ mFactory = factory;
+ mContentCount = contentFilenames.length;
+ mLookupTableFile = new File(baseDir, name + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX);
+ mAddressTableFiles = new File[mContentCount];
+ mContentFiles = new File[mContentCount];
+ for (int i = 0; i < mContentCount; ++i) {
+ mAddressTableFiles[i] = new File(mBaseDir,
+ name + FormatSpec.CONTENT_TABLE_FILE_SUFFIX + contentSuffixes[i]);
+ mContentFiles[i] = new File(mBaseDir, contentFilenames[i] + contentSuffixes[i]);
+ }
+ mAddressTableBuffers = new DictBuffer[mContentCount];
+ mContentBuffers = new DictBuffer[mContentCount];
+ }
+
+ public void openBuffers() throws FileNotFoundException, IOException {
+ mLookupTableBuffer = mFactory.getDictionaryBuffer(mLookupTableFile);
+ for (int i = 0; i < mContentCount; ++i) {
+ mAddressTableBuffers[i] = mFactory.getDictionaryBuffer(mAddressTableFiles[i]);
+ mContentBuffers[i] = mFactory.getDictionaryBuffer(mContentFiles[i]);
+ }
+ }
+
+ /**
+ * Calls the read() callback of the reader with the appropriate buffer appropriately positioned.
+ * @param contentNumber the index in the original contentFilenames[] array.
+ * @param terminalId the terminal ID to read.
+ * @param reader the reader on which to call the callback.
+ */
+ protected void read(final int contentNumber, final int terminalId,
+ final SparseTableContentReaderInterface reader) {
+ if (terminalId < 0 || (terminalId / mBlockSize) * SparseTable.SIZE_OF_INT_IN_BYTES
+ >= mLookupTableBuffer.limit()) {
+ return;
+ }
+
+ mLookupTableBuffer.position((terminalId / mBlockSize) * SparseTable.SIZE_OF_INT_IN_BYTES);
+ final int indexInAddressTable = mLookupTableBuffer.readInt();
+ if (indexInAddressTable == SparseTable.NOT_EXIST) {
+ return;
+ }
+
+ mAddressTableBuffers[contentNumber].position(SparseTable.SIZE_OF_INT_IN_BYTES
+ * ((indexInAddressTable * mBlockSize) + (terminalId % mBlockSize)));
+ final int address = mAddressTableBuffers[contentNumber].readInt();
+ if (address == SparseTable.NOT_EXIST) {
+ return;
+ }
+
+ mContentBuffers[contentNumber].position(address);
+ reader.read(mContentBuffers[contentNumber]);
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/makedict/SparseTableContentWriter.java b/java/src/com/android/inputmethod/latin/makedict/SparseTableContentWriter.java
new file mode 100644
index 000000000..49f0fd624
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/SparseTableContentWriter.java
@@ -0,0 +1,93 @@
+/*
+ * 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.makedict;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An auxiliary class for writing data associated with SparseTable to files.
+ */
+public class SparseTableContentWriter {
+ public interface SparseTableContentWriterInterface {
+ public void write(final OutputStream outStream) throws IOException;
+ }
+
+ private final int mContentCount;
+ private final SparseTable mSparseTable;
+ private final File mLookupTableFile;
+ protected final File mBaseDir;
+ private final File[] mAddressTableFiles;
+ private final File[] mContentFiles;
+ protected final OutputStream[] mContentOutStreams;
+
+ /**
+ * Sole constructor of SparseTableContentWriter.
+ *
+ * @param name the name of SparseTable.
+ * @param initialCapacity the initial capacity of SparseTable.
+ * @param blockSize the block size of the content table.
+ * @param baseDir the directory which contains the files of the content table.
+ * @param contentFilenames the file names of content files.
+ * @param contentIds the ids of contents. These ids are used for a suffix of a name of address
+ * files and content files.
+ */
+ public SparseTableContentWriter(final String name, final int initialCapacity,
+ final int blockSize, final File baseDir, final String[] contentFilenames,
+ final String[] contentIds) {
+ if (contentFilenames.length != contentIds.length) {
+ throw new RuntimeException("The length of contentFilenames and the length of"
+ + " contentIds are different " + contentFilenames.length + ", "
+ + contentIds.length);
+ }
+ mContentCount = contentFilenames.length;
+ mSparseTable = new SparseTable(initialCapacity, blockSize, mContentCount);
+ mLookupTableFile = new File(baseDir, name + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX);
+ mAddressTableFiles = new File[mContentCount];
+ mContentFiles = new File[mContentCount];
+ mBaseDir = baseDir;
+ for (int i = 0; i < mContentCount; ++i) {
+ mAddressTableFiles[i] = new File(mBaseDir,
+ name + FormatSpec.CONTENT_TABLE_FILE_SUFFIX + contentIds[i]);
+ mContentFiles[i] = new File(mBaseDir, contentFilenames[i] + contentIds[i]);
+ }
+ mContentOutStreams = new OutputStream[mContentCount];
+ }
+
+ public void openStreams() throws FileNotFoundException {
+ for (int i = 0; i < mContentCount; ++i) {
+ mContentOutStreams[i] = new FileOutputStream(mContentFiles[i]);
+ }
+ }
+
+ protected void write(final int contentIndex, final int index,
+ final SparseTableContentWriterInterface writer) throws IOException {
+ mSparseTable.set(contentIndex, index, (int) mContentFiles[contentIndex].length());
+ writer.write(mContentOutStreams[contentIndex]);
+ mContentOutStreams[contentIndex].flush();
+ }
+
+ public void closeStreams() throws IOException {
+ mSparseTable.writeToFiles(mLookupTableFile, mAddressTableFiles);
+ for (int i = 0; i < mContentCount; ++i) {
+ mContentOutStreams[i].close();
+ }
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver2DictDecoder.java
index acab4f8a5..ea0a2c6c2 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver2DictDecoder.java
@@ -23,7 +23,6 @@ import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
-import com.android.inputmethod.latin.utils.JniUtils;
import android.util.Log;
@@ -34,18 +33,11 @@ import java.util.ArrayList;
import java.util.Arrays;
/**
- * An implementation of DictDecoder for version 3 binary dictionary.
+ * An implementation of DictDecoder for version 2 binary dictionary.
*/
@UsedForTesting
-public class Ver3DictDecoder extends AbstractDictDecoder {
- private static final String TAG = Ver3DictDecoder.class.getSimpleName();
-
- static {
- JniUtils.loadNativeLibrary();
- }
-
- // TODO: implement something sensical instead of just a phony method
- private static native int doNothing();
+public class Ver2DictDecoder extends AbstractDictDecoder {
+ private static final String TAG = Ver2DictDecoder.class.getSimpleName();
protected static class PtNodeReader extends AbstractDictDecoder.PtNodeReader {
private static int readFrequency(final DictBuffer dictBuffer) {
@@ -57,7 +49,7 @@ public class Ver3DictDecoder extends AbstractDictDecoder {
private final DictionaryBufferFactory mBufferFactory;
protected DictBuffer mDictBuffer;
- /* package */ Ver3DictDecoder(final File file, final int factoryFlag) {
+ /* package */ Ver2DictDecoder(final File file, final int factoryFlag) {
mDictionaryBinaryFile = file;
mDictBuffer = null;
@@ -72,7 +64,7 @@ public class Ver3DictDecoder extends AbstractDictDecoder {
}
}
- /* package */ Ver3DictDecoder(final File file, final DictionaryBufferFactory factory) {
+ /* package */ Ver2DictDecoder(final File file, final DictionaryBufferFactory factory) {
mDictionaryBinaryFile = file;
mBufferFactory = factory;
}
@@ -166,7 +158,7 @@ public class Ver3DictDecoder extends AbstractDictDecoder {
final ArrayList<PendingAttribute> bigrams;
if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) {
bigrams = new ArrayList<PendingAttribute>();
- addressPointer += PtNodeReader.readBigramAddresses(mDictBuffer, bigrams,
+ addressPointer += PtNodeReader.readBigramAddresses(mDictBuffer, bigrams,
addressPointer);
if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
throw new RuntimeException("Too many bigrams in a PtNode (" + bigrams.size()
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver2DictEncoder.java
index 5da34534e..e5430423d 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver2DictEncoder.java
@@ -31,16 +31,16 @@ import java.util.ArrayList;
import java.util.Iterator;
/**
- * An implementation of DictEncoder for version 3 binary dictionary.
+ * An implementation of DictEncoder for version 2 binary dictionary.
*/
-public class Ver3DictEncoder implements DictEncoder {
+public class Ver2DictEncoder implements DictEncoder {
private final File mDictFile;
private OutputStream mOutStream;
private byte[] mBuffer;
private int mPosition;
- public Ver3DictEncoder(final File dictFile) {
+ public Ver2DictEncoder(final File dictFile) {
mDictFile = dictFile;
mOutStream = null;
mBuffer = null;
@@ -49,7 +49,7 @@ public class Ver3DictEncoder implements DictEncoder {
// This constructor is used only by BinaryDictOffdeviceUtilsTests.
// If you want to use this in the production code, you should consider keeping consistency of
// the interface of Ver3DictDecoder by using factory.
- public Ver3DictEncoder(final OutputStream outStream) {
+ public Ver2DictEncoder(final OutputStream outStream) {
mDictFile = null;
mOutStream = outStream;
}
@@ -68,7 +68,7 @@ public class Ver3DictEncoder implements DictEncoder {
@Override
public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions)
throws IOException, UnsupportedFormatException {
- if (formatOptions.mVersion > FormatSpec.VERSION3) {
+ if (formatOptions.mVersion > FormatSpec.VERSION2) {
throw new UnsupportedFormatException(
"The given format options has wrong version number : "
+ formatOptions.mVersion);
@@ -169,7 +169,7 @@ public class Ver3DictEncoder implements DictEncoder {
private void writeChildrenPosition(final PtNode ptNode, final FormatOptions formatOptions) {
final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions);
- if (formatOptions.mSupportsDynamicUpdate) {
+ if (formatOptions.supportsDynamicUpdate()) {
mPosition += BinaryDictEncoderUtils.writeSignedChildrenPosition(mBuffer, mPosition,
childrenPos);
} else {
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java
deleted file mode 100644
index 07adda625..000000000
--- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java
+++ /dev/null
@@ -1,82 +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.makedict;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.ArrayList;
-
-/**
- * An implementation of DictUpdater for version 3 binary dictionary.
- */
-@UsedForTesting
-public class Ver3DictUpdater extends Ver3DictDecoder implements DictUpdater {
- private OutputStream mOutStream;
-
- @UsedForTesting
- public Ver3DictUpdater(final File dictFile, final int factoryType) {
- // DictUpdater must have an updatable DictBuffer.
- super(dictFile, ((factoryType & MASK_DICTBUFFER) == USE_BYTEARRAY)
- ? USE_BYTEARRAY : USE_WRITABLE_BYTEBUFFER);
- mOutStream = null;
- }
-
- private void openStreamAndBuffer() throws FileNotFoundException, IOException {
- super.openDictBuffer();
- mOutStream = new FileOutputStream(mDictionaryBinaryFile, true /* append */);
- }
-
- private void close() throws IOException {
- if (mOutStream != null) {
- mOutStream.close();
- mOutStream = null;
- }
- }
-
- @Override @UsedForTesting
- public void deleteWord(final String word) throws IOException, UnsupportedFormatException {
- if (mOutStream == null) openStreamAndBuffer();
- mDictBuffer.position(0);
- readHeader();
- final int wordPos = getTerminalPosition(word);
- if (wordPos != FormatSpec.NOT_VALID_WORD) {
- mDictBuffer.position(wordPos);
- final int flags = mDictBuffer.readUnsignedByte();
- mDictBuffer.position(wordPos);
- mDictBuffer.put((byte) DynamicBinaryDictIOUtils.markAsDeleted(flags));
- }
- close();
- }
-
- @Override @UsedForTesting
- public void insertWord(final String word, final int frequency,
- final ArrayList<WeightedString> bigramStrings,
- final ArrayList<WeightedString> shortcuts,
- final boolean isNotAWord, final boolean isBlackListEntry)
- throws IOException, UnsupportedFormatException {
- if (mOutStream == null) openStreamAndBuffer();
- DynamicBinaryDictIOUtils.insertWord(this, mOutStream, word, frequency, bigramStrings,
- shortcuts, isNotAWord, isBlackListEntry);
- close();
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
index 734223ec2..7071893d2 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
@@ -40,26 +40,52 @@ import java.util.Arrays;
public class Ver4DictDecoder extends AbstractDictDecoder {
private static final String TAG = Ver4DictDecoder.class.getSimpleName();
- private static final int FILETYPE_TRIE = 1;
- private static final int FILETYPE_FREQUENCY = 2;
- private static final int FILETYPE_TERMINAL_ADDRESS_TABLE = 3;
- private static final int FILETYPE_BIGRAM_FREQ = 4;
- private static final int FILETYPE_SHORTCUT = 5;
-
- private final File mDictDirectory;
- private final DictionaryBufferFactory mBufferFactory;
+ protected static final int FILETYPE_TRIE = 1;
+ protected static final int FILETYPE_FREQUENCY = 2;
+ protected static final int FILETYPE_TERMINAL_ADDRESS_TABLE = 3;
+ protected static final int FILETYPE_BIGRAM_FREQ = 4;
+ protected static final int FILETYPE_SHORTCUT = 5;
+ protected static final int FILETYPE_HEADER = 6;
+
+ protected final File mDictDirectory;
+ protected final DictionaryBufferFactory mBufferFactory;
protected DictBuffer mDictBuffer;
- private DictBuffer mFrequencyBuffer;
- private DictBuffer mTerminalAddressTableBuffer;
- private DictBuffer mBigramBuffer;
- private DictBuffer mShortcutBuffer;
- private SparseTable mBigramAddressTable;
- private SparseTable mShortcutAddressTable;
+ protected DictBuffer mHeaderBuffer;
+ protected DictBuffer mFrequencyBuffer;
+ protected DictBuffer mTerminalAddressTableBuffer;
+ private BigramContentReader mBigramReader;
+ private ShortcutContentReader mShortcutReader;
+
+ /**
+ * Raw PtNode info straight out of a trie file in version 4 dictionary.
+ */
+ protected static final class Ver4PtNodeInfo {
+ public final int mFlags;
+ public final int[] mCharacters;
+ public final int mTerminalId;
+ public final int mChildrenPos;
+ public final int mParentPos;
+ public final int mNodeSize;
+ public int mStartIndexOfCharacters;
+ public int mEndIndexOfCharacters; // exclusive
+
+ public Ver4PtNodeInfo(final int flags, final int[] characters, final int terminalId,
+ final int childrenPos, final int parentPos, final int nodeSize) {
+ mFlags = flags;
+ mCharacters = characters;
+ mTerminalId = terminalId;
+ mChildrenPos = childrenPos;
+ mParentPos = parentPos;
+ mNodeSize = nodeSize;
+ mStartIndexOfCharacters = 0;
+ mEndIndexOfCharacters = characters.length;
+ }
+ }
@UsedForTesting
/* package */ Ver4DictDecoder(final File dictDirectory, final int factoryFlag) {
mDictDirectory = dictDirectory;
- mDictBuffer = mFrequencyBuffer = null;
+ mDictBuffer = mHeaderBuffer = mFrequencyBuffer = null;
if ((factoryFlag & MASK_DICTBUFFER) == USE_READONLY_BYTEBUFFER) {
mBufferFactory = new DictionaryBufferFromReadOnlyByteBufferFactory();
@@ -76,13 +102,16 @@ public class Ver4DictDecoder extends AbstractDictDecoder {
/* package */ Ver4DictDecoder(final File dictDirectory, final DictionaryBufferFactory factory) {
mDictDirectory = dictDirectory;
mBufferFactory = factory;
- mDictBuffer = mFrequencyBuffer = null;
+ mDictBuffer = mHeaderBuffer = mFrequencyBuffer = null;
}
- private File getFile(final int fileType) {
+ protected File getFile(final int fileType) throws UnsupportedFormatException {
if (fileType == FILETYPE_TRIE) {
return new File(mDictDirectory,
mDictDirectory.getName() + FormatSpec.TRIE_FILE_EXTENSION);
+ } else if (fileType == FILETYPE_HEADER) {
+ return new File(mDictDirectory,
+ mDictDirectory.getName() + FormatSpec.HEADER_FILE_EXTENSION);
} else if (fileType == FILETYPE_FREQUENCY) {
return new File(mDictDirectory,
mDictDirectory.getName() + FormatSpec.FREQ_FILE_EXTENSION);
@@ -98,20 +127,27 @@ public class Ver4DictDecoder extends AbstractDictDecoder {
mDictDirectory.getName() + FormatSpec.SHORTCUT_FILE_EXTENSION
+ FormatSpec.SHORTCUT_CONTENT_ID);
} else {
- throw new RuntimeException("Unsupported kind of file : " + fileType);
+ throw new UnsupportedFormatException("Unsupported kind of file : " + fileType);
}
}
@Override
- public void openDictBuffer() throws FileNotFoundException, IOException {
+ public void openDictBuffer() throws FileNotFoundException, IOException,
+ UnsupportedFormatException {
+ if (!mDictDirectory.isDirectory()) {
+ throw new UnsupportedFormatException("Format 4 dictionary needs a directory");
+ }
+ mHeaderBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_HEADER));
mDictBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_TRIE));
mFrequencyBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_FREQUENCY));
mTerminalAddressTableBuffer = mBufferFactory.getDictionaryBuffer(
getFile(FILETYPE_TERMINAL_ADDRESS_TABLE));
- mBigramBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_BIGRAM_FREQ));
- loadBigramAddressSparseTable();
- mShortcutBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_SHORTCUT));
- loadShortcutAddressSparseTable();
+ mBigramReader = new BigramContentReader(mDictDirectory.getName(),
+ mDictDirectory, mBufferFactory, false);
+ mBigramReader.openBuffers();
+ mShortcutReader = new ShortcutContentReader(mDictDirectory.getName(), mDictDirectory,
+ mBufferFactory);
+ mShortcutReader.openBuffers();
}
@Override
@@ -119,46 +155,134 @@ public class Ver4DictDecoder extends AbstractDictDecoder {
return mDictBuffer != null;
}
+ @UsedForTesting
+ /* package */ DictBuffer getHeaderBuffer() {
+ return mHeaderBuffer;
+ }
+
+ @UsedForTesting
/* package */ DictBuffer getDictBuffer() {
return mDictBuffer;
}
@Override
public FileHeader readHeader() throws IOException, UnsupportedFormatException {
- if (mDictBuffer == null) {
+ if (mHeaderBuffer == null) {
openDictBuffer();
}
- final FileHeader header = super.readHeader(mDictBuffer);
+ mHeaderBuffer.position(0);
+ final FileHeader header = super.readHeader(mHeaderBuffer);
final int version = header.mFormatOptions.mVersion;
- if (version != 4) {
+ if (version != FormatSpec.VERSION4) {
throw new UnsupportedFormatException("File header has a wrong version : " + version);
}
return header;
}
- private void loadBigramAddressSparseTable() throws IOException {
- final File lookupIndexFile = new File(mDictDirectory, mDictDirectory.getName()
- + FormatSpec.BIGRAM_FILE_EXTENSION + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX);
- final File freqsFile = new File(mDictDirectory, mDictDirectory.getName()
- + FormatSpec.BIGRAM_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX
- + FormatSpec.BIGRAM_FREQ_CONTENT_ID);
- mBigramAddressTable = SparseTable.readFromFiles(lookupIndexFile, new File[] { freqsFile },
- FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE);
+ /**
+ * An auxiliary class for reading bigrams.
+ */
+ protected static class BigramContentReader extends SparseTableContentReader {
+ public BigramContentReader(final String name, final File baseDir,
+ final DictionaryBufferFactory factory, final boolean hasTimestamp) {
+ super(name + FormatSpec.BIGRAM_FILE_EXTENSION,
+ FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE, baseDir,
+ getContentFilenames(name, hasTimestamp), getContentIds(hasTimestamp), factory);
+ }
+
+ // TODO: Consolidate this method and BigramContentWriter.getContentFilenames.
+ protected static String[] getContentFilenames(final String name,
+ final boolean hasTimestamp) {
+ final String[] contentFilenames;
+ if (hasTimestamp) {
+ contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION,
+ name + FormatSpec.BIGRAM_FILE_EXTENSION };
+ } else {
+ contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION };
+ }
+ return contentFilenames;
+ }
+
+ // TODO: Consolidate this method and BigramContentWriter.getContentIds.
+ protected static String[] getContentIds(final boolean hasTimestamp) {
+ final String[] contentIds;
+ if (hasTimestamp) {
+ contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID,
+ FormatSpec.BIGRAM_TIMESTAMP_CONTENT_ID };
+ } else {
+ contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID };
+ }
+ return contentIds;
+ }
+
+ public ArrayList<PendingAttribute> readTargetsAndFrequencies(final int terminalId,
+ final DictBuffer terminalAddressTableBuffer) {
+ final ArrayList<PendingAttribute> bigrams = CollectionUtils.newArrayList();
+ read(FormatSpec.BIGRAM_FREQ_CONTENT_INDEX, terminalId,
+ new SparseTableContentReaderInterface() {
+ @Override
+ public void read(final DictBuffer buffer) {
+ while (bigrams.size() < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
+ // If bigrams.size() reaches FormatSpec.MAX_BIGRAMS_IN_A_PTNODE,
+ // remaining bigram entries are ignored.
+ final int bigramFlags = buffer.readUnsignedByte();
+ final int targetTerminalId = buffer.readUnsignedInt24();
+ terminalAddressTableBuffer.position(targetTerminalId
+ * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE);
+ final int targetAddress =
+ terminalAddressTableBuffer.readUnsignedInt24();
+ bigrams.add(new PendingAttribute(bigramFlags
+ & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY,
+ targetAddress));
+ if (0 == (bigramFlags
+ & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) {
+ break;
+ }
+ }
+ if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
+ throw new RuntimeException("Too many bigrams in a PtNode ("
+ + bigrams.size() + " but max is "
+ + FormatSpec.MAX_BIGRAMS_IN_A_PTNODE + ")");
+ }
+ }
+ });
+ if (bigrams.isEmpty()) return null;
+ return bigrams;
+ }
}
- // TODO: Let's have something like SparseTableContentsReader in this class.
- private void loadShortcutAddressSparseTable() throws IOException {
- final File lookupIndexFile = new File(mDictDirectory, mDictDirectory.getName()
- + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX);
- final File contentFile = new File(mDictDirectory, mDictDirectory.getName()
- + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX
- + FormatSpec.SHORTCUT_CONTENT_ID);
- final File timestampsFile = new File(mDictDirectory, mDictDirectory.getName()
- + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX
- + FormatSpec.SHORTCUT_CONTENT_ID);
- mShortcutAddressTable = SparseTable.readFromFiles(lookupIndexFile,
- new File[] { contentFile, timestampsFile },
- FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE);
+ /**
+ * An auxiliary class for reading shortcuts.
+ */
+ protected static class ShortcutContentReader extends SparseTableContentReader {
+ public ShortcutContentReader(final String name, final File baseDir,
+ final DictionaryBufferFactory factory) {
+ super(name + FormatSpec.SHORTCUT_FILE_EXTENSION,
+ FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE, baseDir,
+ new String[] { name + FormatSpec.SHORTCUT_FILE_EXTENSION },
+ new String[] { FormatSpec.SHORTCUT_CONTENT_ID }, factory);
+ }
+
+ public ArrayList<WeightedString> readShortcuts(final int terminalId) {
+ final ArrayList<WeightedString> shortcuts = CollectionUtils.newArrayList();
+ read(FormatSpec.SHORTCUT_CONTENT_INDEX, terminalId,
+ new SparseTableContentReaderInterface() {
+ @Override
+ public void read(final DictBuffer buffer) {
+ while (true) {
+ final int flags = buffer.readUnsignedByte();
+ final String word = CharEncoding.readString(buffer);
+ shortcuts.add(new WeightedString(word,
+ flags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY));
+ if (0 == (flags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) {
+ break;
+ }
+ }
+ }
+ });
+ if (shortcuts.isEmpty()) return null;
+ return shortcuts;
+ }
}
protected static class PtNodeReader extends AbstractDictDecoder.PtNodeReader {
@@ -172,102 +296,82 @@ public class Ver4DictDecoder extends AbstractDictDecoder {
}
}
- private ArrayList<WeightedString> readShortcuts(final int terminalId) {
- if (mShortcutAddressTable.get(0, terminalId) == SparseTable.NOT_EXIST) return null;
-
- final ArrayList<WeightedString> ret = CollectionUtils.newArrayList();
- final int posOfShortcuts = mShortcutAddressTable.get(FormatSpec.SHORTCUT_CONTENT_INDEX,
- terminalId);
- mShortcutBuffer.position(posOfShortcuts);
- while (true) {
- final int flags = mShortcutBuffer.readUnsignedByte();
- final String word = CharEncoding.readString(mShortcutBuffer);
- ret.add(new WeightedString(word,
- flags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY));
- if (0 == (flags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break;
- }
- return ret;
- }
+ private final int[] mCharacterBufferForReadingVer4PtNodeInfo
+ = new int[FormatSpec.MAX_WORD_LENGTH];
+ /**
+ * Reads PtNode from ptNodePos in the trie file and returns Ver4PtNodeInfo.
+ *
+ * @param ptNodePos the position of PtNode.
+ * @param options the format options.
+ * @return Ver4PtNodeInfo.
+ */
// TODO: Make this buffer thread safe.
// TODO: Support words longer than FormatSpec.MAX_WORD_LENGTH.
- private final int[] mCharacterBuffer = new int[FormatSpec.MAX_WORD_LENGTH];
- @Override
- public PtNodeInfo readPtNode(int ptNodePos, FormatOptions options) {
- int addressPointer = ptNodePos;
+ protected Ver4PtNodeInfo readVer4PtNodeInfo(final int ptNodePos, final FormatOptions options) {
+ int readingPos = ptNodePos;
final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer);
- addressPointer += FormatSpec.PTNODE_FLAGS_SIZE;
+ readingPos += FormatSpec.PTNODE_FLAGS_SIZE;
- final int parentAddress = PtNodeReader.readParentAddress(mDictBuffer, options);
+ final int parentPos = PtNodeReader.readParentAddress(mDictBuffer, options);
if (BinaryDictIOUtils.supportsDynamicUpdate(options)) {
- addressPointer += FormatSpec.PARENT_ADDRESS_SIZE;
+ readingPos += FormatSpec.PARENT_ADDRESS_SIZE;
}
final int characters[];
if (0 != (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS)) {
int index = 0;
int character = CharEncoding.readChar(mDictBuffer);
- addressPointer += CharEncoding.getCharSize(character);
+ readingPos += CharEncoding.getCharSize(character);
while (FormatSpec.INVALID_CHARACTER != character
&& index < FormatSpec.MAX_WORD_LENGTH) {
- mCharacterBuffer[index++] = character;
+ mCharacterBufferForReadingVer4PtNodeInfo[index++] = character;
character = CharEncoding.readChar(mDictBuffer);
- addressPointer += CharEncoding.getCharSize(character);
+ readingPos += CharEncoding.getCharSize(character);
}
- characters = Arrays.copyOfRange(mCharacterBuffer, 0, index);
+ characters = Arrays.copyOfRange(mCharacterBufferForReadingVer4PtNodeInfo, 0, index);
} else {
final int character = CharEncoding.readChar(mDictBuffer);
- addressPointer += CharEncoding.getCharSize(character);
+ readingPos += CharEncoding.getCharSize(character);
characters = new int[] { character };
}
final int terminalId;
if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) {
terminalId = PtNodeReader.readTerminalId(mDictBuffer);
- addressPointer += FormatSpec.PTNODE_TERMINAL_ID_SIZE;
+ readingPos += FormatSpec.PTNODE_TERMINAL_ID_SIZE;
} else {
terminalId = PtNode.NOT_A_TERMINAL;
}
+ int childrenPos = PtNodeReader.readChildrenAddress(mDictBuffer, flags, options);
+ if (childrenPos != FormatSpec.NO_CHILDREN_ADDRESS) {
+ childrenPos += readingPos;
+ }
+ readingPos += BinaryDictIOUtils.getChildrenAddressSize(flags, options);
+
+ return new Ver4PtNodeInfo(flags, characters, terminalId, childrenPos, parentPos,
+ readingPos - ptNodePos);
+ }
+
+ @Override
+ public PtNodeInfo readPtNode(int ptNodePos, FormatOptions options) {
+ final Ver4PtNodeInfo nodeInfo = readVer4PtNodeInfo(ptNodePos, options);
+
final int frequency;
- if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) {
- frequency = PtNodeReader.readFrequency(mFrequencyBuffer, terminalId);
+ if (0 != (FormatSpec.FLAG_IS_TERMINAL & nodeInfo.mFlags)) {
+ frequency = PtNodeReader.readFrequency(mFrequencyBuffer, nodeInfo.mTerminalId);
} else {
frequency = PtNode.NOT_A_TERMINAL;
}
- int childrenAddress = PtNodeReader.readChildrenAddress(mDictBuffer, flags, options);
- if (childrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
- childrenAddress += addressPointer;
- }
- addressPointer += BinaryDictIOUtils.getChildrenAddressSize(flags, options);
- final ArrayList<WeightedString> shortcutTargets = readShortcuts(terminalId);
-
- final ArrayList<PendingAttribute> bigrams;
- if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) {
- bigrams = new ArrayList<PendingAttribute>();
- final int posOfBigrams = mBigramAddressTable.get(0 /* contentTableIndex */, terminalId);
- mBigramBuffer.position(posOfBigrams);
- while (bigrams.size() < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
- // If bigrams.size() reaches FormatSpec.MAX_BIGRAMS_IN_A_PTNODE,
- // remaining bigram entries are ignored.
- final int bigramFlags = mBigramBuffer.readUnsignedByte();
- final int targetTerminalId = mBigramBuffer.readUnsignedInt24();
- mTerminalAddressTableBuffer.position(
- targetTerminalId * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE);
- final int targetAddress = mTerminalAddressTableBuffer.readUnsignedInt24();
- bigrams.add(new PendingAttribute(
- bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY,
- targetAddress));
- if (0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break;
- }
- if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
- throw new RuntimeException("Too many bigrams in a PtNode (" + bigrams.size()
- + " but max is " + FormatSpec.MAX_BIGRAMS_IN_A_PTNODE + ")");
- }
- } else {
- bigrams = null;
- }
- return new PtNodeInfo(ptNodePos, addressPointer, flags, characters, frequency,
- parentAddress, childrenAddress, shortcutTargets, bigrams);
+
+ final ArrayList<WeightedString> shortcutTargets = mShortcutReader.readShortcuts(
+ nodeInfo.mTerminalId);
+ final ArrayList<PendingAttribute> bigrams = mBigramReader.readTargetsAndFrequencies(
+ nodeInfo.mTerminalId, mTerminalAddressTableBuffer);
+
+ return new PtNodeInfo(ptNodePos, ptNodePos + nodeInfo.mNodeSize, nodeInfo.mFlags,
+ nodeInfo.mCharacters, frequency, nodeInfo.mParentPos, nodeInfo.mChildrenPos,
+ shortcutTargets, bigrams);
}
private void deleteDictFiles() {
@@ -318,10 +422,14 @@ public class Ver4DictDecoder extends AbstractDictDecoder {
@Override
public boolean readAndFollowForwardLink() {
- final int nextAddress = mDictBuffer.readUnsignedInt24();
- if (nextAddress >= 0 && nextAddress < mDictBuffer.limit()) {
- mDictBuffer.position(nextAddress);
- return true;
+ final int forwardLinkPos = mDictBuffer.position();
+ int nextRelativePos = BinaryDictDecoderUtils.readSInt24(mDictBuffer);
+ if (nextRelativePos != FormatSpec.NO_FORWARD_LINK_ADDRESS) {
+ final int nextPos = forwardLinkPos + nextRelativePos;
+ if (nextPos >= 0 && nextPos < mDictBuffer.limit()) {
+ mDictBuffer.position(nextPos);
+ return true;
+ }
}
return false;
}
@@ -332,6 +440,7 @@ public class Ver4DictDecoder extends AbstractDictDecoder {
}
@Override
+ @UsedForTesting
public void skipPtNode(final FormatOptions formatOptions) {
final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer);
PtNodeReader.readParentAddress(mDictBuffer, formatOptions);
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
index 8d5b48a9b..d34aa171e 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
@@ -1,5 +1,4 @@
/*
-/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,21 +17,15 @@
package com.android.inputmethod.latin.makedict;
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding;
-import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
-import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
-import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+import com.android.inputmethod.latin.utils.LocaleUtils;
import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Iterator;
/**
* An implementation of DictEncoder for version 4 binary dictionary.
@@ -40,244 +33,19 @@ import java.util.Iterator;
@UsedForTesting
public class Ver4DictEncoder implements DictEncoder {
private final File mDictPlacedDir;
- private byte[] mTrieBuf;
- private int mTriePos;
- private int mHeaderSize;
- private OutputStream mTrieOutStream;
- private OutputStream mFreqOutStream;
- private OutputStream mUnigramTimestampOutStream;
- private OutputStream mTerminalAddressTableOutStream;
- private File mDictDir;
- private String mBaseFilename;
- private BigramContentWriter mBigramWriter;
- private ShortcutContentWriter mShortcutWriter;
@UsedForTesting
public Ver4DictEncoder(final File dictPlacedDir) {
mDictPlacedDir = dictPlacedDir;
}
- private interface SparseTableContentWriterInterface {
- public void write(final OutputStream outStream) throws IOException;
- }
-
- private static class SparseTableContentWriter {
- private final int mContentCount;
- private final SparseTable mSparseTable;
- private final File mLookupTableFile;
- protected final File mBaseDir;
- private final File[] mAddressTableFiles;
- private final File[] mContentFiles;
- protected final OutputStream[] mContentOutStreams;
-
- public SparseTableContentWriter(final String name, final int initialCapacity,
- final int blockSize, final File baseDir, final String[] contentFilenames,
- final String[] contentIds) {
- if (contentFilenames.length != contentIds.length) {
- throw new RuntimeException("The length of contentFilenames and the length of"
- + " contentIds are different " + contentFilenames.length + ", "
- + contentIds.length);
- }
- mContentCount = contentFilenames.length;
- mSparseTable = new SparseTable(initialCapacity, blockSize, mContentCount);
- mLookupTableFile = new File(baseDir, name + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX);
- mAddressTableFiles = new File[mContentCount];
- mContentFiles = new File[mContentCount];
- mBaseDir = baseDir;
- for (int i = 0; i < mContentCount; ++i) {
- mAddressTableFiles[i] = new File(mBaseDir,
- name + FormatSpec.CONTENT_TABLE_FILE_SUFFIX + contentIds[i]);
- mContentFiles[i] = new File(mBaseDir, contentFilenames[i] + contentIds[i]);
- }
- mContentOutStreams = new OutputStream[mContentCount];
- }
-
- public void openStreams() throws FileNotFoundException {
- for (int i = 0; i < mContentCount; ++i) {
- mContentOutStreams[i] = new FileOutputStream(mContentFiles[i]);
- }
- }
-
- protected void write(final int contentIndex, final int index,
- final SparseTableContentWriterInterface writer) throws IOException {
- mSparseTable.set(contentIndex, index, (int) mContentFiles[contentIndex].length());
- writer.write(mContentOutStreams[contentIndex]);
- mContentOutStreams[contentIndex].flush();
- }
-
- public void closeStreams() throws IOException {
- mSparseTable.writeToFiles(mLookupTableFile, mAddressTableFiles);
- for (int i = 0; i < mContentCount; ++i) {
- mContentOutStreams[i].close();
- }
- }
- }
-
- private static class BigramContentWriter extends SparseTableContentWriter {
- private final boolean mWriteTimestamp;
-
- public BigramContentWriter(final String name, final int initialCapacity,
- final File baseDir, final boolean writeTimestamp) {
- super(name + FormatSpec.BIGRAM_FILE_EXTENSION, initialCapacity,
- FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE, baseDir,
- getContentFilenames(name, writeTimestamp), getContentIds(writeTimestamp));
- mWriteTimestamp = writeTimestamp;
- }
-
- private static String[] getContentFilenames(final String name,
- final boolean writeTimestamp) {
- final String[] contentFilenames;
- if (writeTimestamp) {
- contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION,
- name + FormatSpec.BIGRAM_FILE_EXTENSION };
- } else {
- contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION };
- }
- return contentFilenames;
- }
-
- private static String[] getContentIds(final boolean writeTimestamp) {
- final String[] contentIds;
- if (writeTimestamp) {
- contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID,
- FormatSpec.BIGRAM_TIMESTAMP_CONTENT_ID };
- } else {
- contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID };
- }
- return contentIds;
- }
-
- public void writeBigramsForOneWord(final int terminalId, final int bigramCount,
- final Iterator<WeightedString> bigramIterator, final FusionDictionary dict)
- throws IOException {
- write(FormatSpec.BIGRAM_FREQ_CONTENT_INDEX, terminalId,
- new SparseTableContentWriterInterface() {
- @Override
- public void write(final OutputStream outStream) throws IOException {
- writeBigramsForOneWordInternal(outStream, bigramIterator, dict);
- }});
- if (mWriteTimestamp) {
- write(FormatSpec.BIGRAM_TIMESTAMP_CONTENT_INDEX, terminalId,
- new SparseTableContentWriterInterface() {
- @Override
- public void write(final OutputStream outStream) throws IOException {
- initBigramTimestampsCountersAndLevelsForOneWordInternal(outStream,
- bigramCount);
- }});
- }
- }
-
- private void writeBigramsForOneWordInternal(final OutputStream outStream,
- final Iterator<WeightedString> bigramIterator, final FusionDictionary dict)
- throws IOException {
- while (bigramIterator.hasNext()) {
- final WeightedString bigram = bigramIterator.next();
- final PtNode target =
- FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord);
- final int unigramFrequencyForThisWord = target.mFrequency;
- final int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(
- bigramIterator.hasNext(), 0, bigram.mFrequency,
- unigramFrequencyForThisWord, bigram.mWord);
- BinaryDictEncoderUtils.writeUIntToStream(outStream, bigramFlags,
- FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE);
- BinaryDictEncoderUtils.writeUIntToStream(outStream, target.mTerminalId,
- FormatSpec.PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE);
- }
- }
-
- private void initBigramTimestampsCountersAndLevelsForOneWordInternal(
- final OutputStream outStream, final int bigramCount) throws IOException {
- for (int i = 0; i < bigramCount; ++i) {
- // TODO: Figure out what initial values should be.
- BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */,
- FormatSpec.BIGRAM_TIMESTAMP_SIZE);
- BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */,
- FormatSpec.BIGRAM_COUNTER_SIZE);
- BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */,
- FormatSpec.BIGRAM_LEVEL_SIZE);
- }
- }
- }
-
- private static class ShortcutContentWriter extends SparseTableContentWriter {
- public ShortcutContentWriter(final String name, final int initialCapacity,
- final File baseDir) {
- super(name + FormatSpec.SHORTCUT_FILE_EXTENSION, initialCapacity,
- FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE, baseDir,
- new String[] { name + FormatSpec.SHORTCUT_FILE_EXTENSION },
- new String[] { FormatSpec.SHORTCUT_CONTENT_ID });
- }
-
- public void writeShortcutForOneWord(final int terminalId,
- final Iterator<WeightedString> shortcutIterator) throws IOException {
- write(FormatSpec.SHORTCUT_CONTENT_INDEX, terminalId,
- new SparseTableContentWriterInterface() {
- @Override
- public void write(final OutputStream outStream) throws IOException {
- writeShortcutForOneWordInternal(outStream, shortcutIterator);
- }
- });
- }
-
- private void writeShortcutForOneWordInternal(final OutputStream outStream,
- final Iterator<WeightedString> shortcutIterator) throws IOException {
- while (shortcutIterator.hasNext()) {
- final WeightedString target = shortcutIterator.next();
- final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags(
- shortcutIterator.hasNext(), target.mFrequency);
- BinaryDictEncoderUtils.writeUIntToStream(outStream, shortcutFlags,
- FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE);
- CharEncoding.writeString(outStream, target.mWord);
- }
- }
- }
-
- private void openStreams(final FormatOptions formatOptions, final DictionaryOptions dictOptions)
- throws FileNotFoundException, IOException {
- final FileHeader header = new FileHeader(0, dictOptions, formatOptions);
- mBaseFilename = header.getId() + "." + header.getVersion();
- mDictDir = new File(mDictPlacedDir, mBaseFilename);
- final File trieFile = new File(mDictDir, mBaseFilename + FormatSpec.TRIE_FILE_EXTENSION);
- final File freqFile = new File(mDictDir, mBaseFilename + FormatSpec.FREQ_FILE_EXTENSION);
- final File timestampFile = new File(mDictDir,
- mBaseFilename + FormatSpec.UNIGRAM_TIMESTAMP_FILE_EXTENSION);
- final File terminalAddressTableFile = new File(mDictDir,
- mBaseFilename + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION);
- if (!mDictDir.isDirectory()) {
- if (mDictDir.exists()) mDictDir.delete();
- mDictDir.mkdirs();
- }
- mTrieOutStream = new FileOutputStream(trieFile);
- mFreqOutStream = new FileOutputStream(freqFile);
- mTerminalAddressTableOutStream = new FileOutputStream(terminalAddressTableFile);
- if (formatOptions.mHasTimestamp) {
- mUnigramTimestampOutStream = new FileOutputStream(timestampFile);
- }
- }
-
- private void close() throws IOException {
- try {
- if (mTrieOutStream != null) {
- mTrieOutStream.close();
- }
- if (mFreqOutStream != null) {
- mFreqOutStream.close();
- }
- if (mTerminalAddressTableOutStream != null) {
- mTerminalAddressTableOutStream.close();
- }
- if (mUnigramTimestampOutStream != null) {
- mUnigramTimestampOutStream.close();
- }
- } finally {
- mTrieOutStream = null;
- mFreqOutStream = null;
- mTerminalAddressTableOutStream = null;
- }
- }
-
+ // TODO: This builds a FusionDictionary first and iterates it to add words to the binary
+ // dictionary. However, it is possible to just add words directly to the binary dictionary
+ // instead.
+ // In the long run, when we stop supporting version 2, FusionDictionary will become deprecated
+ // and we can remove it. Then we'll be able to just call BinaryDictionary directly.
@Override
- public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions)
+ public void writeDictionary(FusionDictionary dict, FormatOptions formatOptions)
throws IOException, UnsupportedFormatException {
if (formatOptions.mVersion != FormatSpec.VERSION4) {
throw new UnsupportedFormatException("File header has a wrong version number : "
@@ -286,190 +54,71 @@ public class Ver4DictEncoder implements DictEncoder {
if (!mDictPlacedDir.isDirectory()) {
throw new UnsupportedFormatException("Given path is not a directory.");
}
-
- if (mTrieOutStream == null) {
- openStreams(formatOptions, dict.mOptions);
- }
-
- mHeaderSize = BinaryDictEncoderUtils.writeDictionaryHeader(mTrieOutStream, dict,
- formatOptions);
-
- MakedictLog.i("Flattening the tree...");
- ArrayList<PtNodeArray> flatNodes = BinaryDictEncoderUtils.flattenTree(dict.mRootNodeArray);
- int terminalCount = 0;
- for (final PtNodeArray array : flatNodes) {
- for (final PtNode node : array.mData) {
- if (node.isTerminal()) node.mTerminalId = terminalCount++;
+ if (!BinaryDictionary.createEmptyDictFile(mDictPlacedDir.getAbsolutePath(),
+ FormatSpec.VERSION4, dict.mOptions.mAttributes)) {
+ throw new IOException("Cannot create dictionary file : "
+ + mDictPlacedDir.getAbsolutePath());
+ }
+ final BinaryDictionary binaryDict = new BinaryDictionary(mDictPlacedDir.getAbsolutePath(),
+ 0l, mDictPlacedDir.length(), true /* useFullEditDistance */,
+ LocaleUtils.constructLocaleFromString(dict.mOptions.mAttributes.get(
+ FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE)),
+ Dictionary.TYPE_USER /* Dictionary type. Does not matter for us */,
+ true /* isUpdatable */);
+ if (!binaryDict.isValidDictionary()) {
+ // Somehow createEmptyDictFile returned true, but the file was not created correctly
+ throw new IOException("Cannot create dictionary file");
+ }
+ for (final Word word : dict) {
+ // TODO: switch to addMultipleDictionaryEntries when they support shortcuts
+ if (null == word.mShortcutTargets || word.mShortcutTargets.isEmpty()) {
+ binaryDict.addUnigramWord(word.mWord, word.mFrequency,
+ null /* shortcutTarget */, 0 /* shortcutProbability */,
+ word.mIsNotAWord, word.mIsBlacklistEntry, 0 /* timestamp */);
+ } else {
+ for (final WeightedString shortcutTarget : word.mShortcutTargets) {
+ binaryDict.addUnigramWord(word.mWord, word.mFrequency,
+ shortcutTarget.mWord, shortcutTarget.mFrequency,
+ word.mIsNotAWord, word.mIsBlacklistEntry, 0 /* timestamp */);
+ }
+ }
+ if (binaryDict.needsToRunGC(true /* mindsBlockByGC */)) {
+ binaryDict.flushWithGC();
}
}
-
- MakedictLog.i("Computing addresses...");
- BinaryDictEncoderUtils.computeAddresses(dict, flatNodes, formatOptions);
- if (MakedictLog.DBG) BinaryDictEncoderUtils.checkFlatPtNodeArrayList(flatNodes);
-
- writeTerminalData(flatNodes, terminalCount);
- if (formatOptions.mHasTimestamp) {
- initUnigramTimestamps(terminalCount);
- }
- mBigramWriter = new BigramContentWriter(mBaseFilename, terminalCount, mDictDir,
- formatOptions.mHasTimestamp);
- writeBigrams(flatNodes, dict);
- mShortcutWriter = new ShortcutContentWriter(mBaseFilename, terminalCount, mDictDir);
- writeShortcuts(flatNodes);
-
- final PtNodeArray lastNodeArray = flatNodes.get(flatNodes.size() - 1);
- final int bufferSize = lastNodeArray.mCachedAddressAfterUpdate + lastNodeArray.mCachedSize;
- mTrieBuf = new byte[bufferSize];
-
- MakedictLog.i("Writing file...");
- for (PtNodeArray nodeArray : flatNodes) {
- BinaryDictEncoderUtils.writePlacedPtNodeArray(dict, this, nodeArray, formatOptions);
- }
- if (MakedictLog.DBG) {
- BinaryDictEncoderUtils.showStatistics(flatNodes);
- MakedictLog.i("has " + terminalCount + " terminals.");
+ for (final Word word0 : dict) {
+ if (null == word0.mBigrams) continue;
+ for (final WeightedString word1 : word0.mBigrams) {
+ binaryDict.addBigramWords(word0.mWord, word1.mWord, word1.mFrequency,
+ 0 /* timestamp */);
+ if (binaryDict.needsToRunGC(true /* mindsBlockByGC */)) {
+ binaryDict.flushWithGC();
+ }
+ }
}
- mTrieOutStream.write(mTrieBuf);
-
- MakedictLog.i("Done");
- close();
+ binaryDict.flushWithGC();
+ binaryDict.close();
}
@Override
public void setPosition(int position) {
- if (mTrieBuf == null || position < 0 || position >- mTrieBuf.length) return;
- mTriePos = position;
}
@Override
public int getPosition() {
- return mTriePos;
+ return 0;
}
@Override
public void writePtNodeCount(int ptNodeCount) {
- final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount);
- // ptNodeCount must fit on one byte or two bytes.
- // Please see comments in FormatSpec
- if (countSize != 1 && countSize != 2) {
- throw new RuntimeException("Strange size from getPtNodeCountSize : " + countSize);
- }
- final int encodedPtNodeCount = (countSize == 2) ?
- (ptNodeCount | FormatSpec.LARGE_PTNODE_ARRAY_SIZE_FIELD_SIZE_FLAG) : ptNodeCount;
- mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, encodedPtNodeCount,
- countSize);
- }
-
- private void writePtNodeFlags(final PtNode ptNode, final FormatOptions formatOptions) {
- final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions);
- mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos,
- BinaryDictEncoderUtils.makePtNodeFlags(ptNode, childrenPos, formatOptions),
- FormatSpec.PTNODE_FLAGS_SIZE);
- }
-
- private void writeParentPosition(int parentPos, final PtNode ptNode,
- final FormatOptions formatOptions) {
- if (parentPos != FormatSpec.NO_PARENT_ADDRESS) {
- parentPos -= ptNode.mCachedAddressAfterUpdate;
- }
- mTriePos = BinaryDictEncoderUtils.writeParentAddress(mTrieBuf, mTriePos, parentPos,
- formatOptions);
- }
-
- private void writeCharacters(final int[] characters, final boolean hasSeveralChars) {
- mTriePos = CharEncoding.writeCharArray(characters, mTrieBuf, mTriePos);
- if (hasSeveralChars) {
- mTrieBuf[mTriePos++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR;
- }
- }
-
- private void writeTerminalId(final int terminalId) {
- mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, terminalId,
- FormatSpec.PTNODE_TERMINAL_ID_SIZE);
- }
-
- private void writeChildrenPosition(PtNode ptNode, FormatOptions formatOptions) {
- final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions);
- if (formatOptions.mSupportsDynamicUpdate) {
- mTriePos += BinaryDictEncoderUtils.writeSignedChildrenPosition(mTrieBuf,
- mTriePos, childrenPos);
- } else {
- mTriePos += BinaryDictEncoderUtils.writeChildrenPosition(mTrieBuf,
- mTriePos, childrenPos);
- }
- }
-
- private void writeBigrams(final ArrayList<PtNodeArray> flatNodes, final FusionDictionary dict)
- throws IOException {
- mBigramWriter.openStreams();
- for (final PtNodeArray nodeArray : flatNodes) {
- for (final PtNode ptNode : nodeArray.mData) {
- if (ptNode.mBigrams != null) {
- mBigramWriter.writeBigramsForOneWord(ptNode.mTerminalId, ptNode.mBigrams.size(),
- ptNode.mBigrams.iterator(), dict);
- }
- }
- }
- mBigramWriter.closeStreams();
- }
-
- private void writeShortcuts(final ArrayList<PtNodeArray> flatNodes) throws IOException {
- mShortcutWriter.openStreams();
- for (final PtNodeArray nodeArray : flatNodes) {
- for (final PtNode ptNode : nodeArray.mData) {
- if (ptNode.mShortcutTargets != null && !ptNode.mShortcutTargets.isEmpty()) {
- mShortcutWriter.writeShortcutForOneWord(ptNode.mTerminalId,
- ptNode.mShortcutTargets.iterator());
- }
- }
- }
- mShortcutWriter.closeStreams();
}
@Override
public void writeForwardLinkAddress(int forwardLinkAddress) {
- mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos,
- forwardLinkAddress, FormatSpec.FORWARD_LINK_ADDRESS_SIZE);
}
@Override
- public void writePtNode(final PtNode ptNode, final int parentPosition,
- final FormatOptions formatOptions, final FusionDictionary dict) {
- writePtNodeFlags(ptNode, formatOptions);
- writeParentPosition(parentPosition, ptNode, formatOptions);
- writeCharacters(ptNode.mChars, ptNode.hasSeveralChars());
- if (ptNode.isTerminal()) {
- writeTerminalId(ptNode.mTerminalId);
- }
- writeChildrenPosition(ptNode, formatOptions);
- }
-
- private void writeTerminalData(final ArrayList<PtNodeArray> flatNodes,
- final int terminalCount) throws IOException {
- final byte[] freqBuf = new byte[terminalCount * FormatSpec.FREQUENCY_AND_FLAGS_SIZE];
- final byte[] terminalAddressTableBuf =
- new byte[terminalCount * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE];
- for (final PtNodeArray nodeArray : flatNodes) {
- for (final PtNode ptNode : nodeArray.mData) {
- if (ptNode.isTerminal()) {
- BinaryDictEncoderUtils.writeUIntToBuffer(freqBuf,
- ptNode.mTerminalId * FormatSpec.FREQUENCY_AND_FLAGS_SIZE,
- ptNode.mFrequency, FormatSpec.FREQUENCY_AND_FLAGS_SIZE);
- BinaryDictEncoderUtils.writeUIntToBuffer(terminalAddressTableBuf,
- ptNode.mTerminalId * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE,
- ptNode.mCachedAddressAfterUpdate + mHeaderSize,
- FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE);
- }
- }
- }
- mFreqOutStream.write(freqBuf);
- mTerminalAddressTableOutStream.write(terminalAddressTableBuf);
- }
-
- private void initUnigramTimestamps(final int terminalCount) throws IOException {
- // Initial value of time stamps for each word is 0.
- final byte[] unigramTimestampBuf =
- new byte[terminalCount * FormatSpec.UNIGRAM_TIMESTAMP_SIZE];
- mUnigramTimestampOutStream.write(unigramTimestampBuf);
+ public void writePtNode(
+ PtNode ptNode, int parentPosition, FormatOptions formatOptions, FusionDictionary dict) {
}
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java
deleted file mode 100644
index 3d8f186ba..000000000
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java
+++ /dev/null
@@ -1,59 +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.makedict;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-
-/**
- * An implementation of DictUpdater for version 4 binary dictionary.
- */
-@UsedForTesting
-public class Ver4DictUpdater extends Ver4DictDecoder implements DictUpdater {
-
- @UsedForTesting
- public Ver4DictUpdater(final File dictDirectory, final int factoryType) {
- // DictUpdater must have an updatable DictBuffer.
- super(dictDirectory, ((factoryType & MASK_DICTBUFFER) == USE_BYTEARRAY)
- ? USE_BYTEARRAY : USE_WRITABLE_BYTEBUFFER);
- }
-
- @Override
- public void deleteWord(final String word) throws IOException, UnsupportedFormatException {
- if (mDictBuffer == null) openDictBuffer();
- readHeader();
- final int wordPos = getTerminalPosition(word);
- if (wordPos != FormatSpec.NOT_VALID_WORD) {
- mDictBuffer.position(wordPos);
- final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer);
- mDictBuffer.position(wordPos);
- mDictBuffer.put((byte) DynamicBinaryDictIOUtils.markAsDeleted(flags));
- }
- }
-
- @Override
- public void insertWord(final String word, final int frequency,
- final ArrayList<WeightedString> bigramStrings, final ArrayList<WeightedString> shortcuts,
- final boolean isNotAWord, final boolean isBlackListEntry)
- throws IOException, UnsupportedFormatException {
- // TODO: Implement this method.
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
index 1de15a333..701c29023 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -17,18 +17,16 @@
package com.android.inputmethod.latin.personalization;
import android.content.Context;
-import android.content.SharedPreferences;
import android.util.Log;
import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.BinaryDictionary.LanguageModelParam;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.ExpandableBinaryDictionary;
-import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.makedict.DictDecoder;
import com.android.inputmethod.latin.makedict.FormatSpec;
-import com.android.inputmethod.latin.settings.Settings;
-import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils;
import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.OnAddWordListener;
@@ -36,7 +34,9 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.Locale;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
/**
* This class is a base class of a dictionary that supports decaying for the personalized language
@@ -45,8 +45,7 @@ import java.util.Map;
public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableBinaryDictionary {
private static final String TAG = DecayingExpandableBinaryDictionaryBase.class.getSimpleName();
public static final boolean DBG_SAVE_RESTORE = false;
- private static final boolean DBG_STRESS_TEST = false;
- private static final boolean PROFILE_SAVE_RESTORE = LatinImeLogger.sDBG;
+ private static final boolean DBG_DUMP_ON_CLOSE = false;
/** Any pair being typed or picked */
public static final int FREQUENCY_FOR_TYPED = 2;
@@ -54,52 +53,56 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB
public static final int FREQUENCY_FOR_WORDS_IN_DICTS = FREQUENCY_FOR_TYPED;
public static final int FREQUENCY_FOR_WORDS_NOT_IN_DICTS = Dictionary.NOT_A_PROBABILITY;
- /** Locale for which this user history dictionary is storing words */
- private final String mLocale;
+ public static final int REQUIRED_BINARY_DICTIONARY_VERSION = FormatSpec.VERSION4;
- private final String mFileName;
+ /** The locale for this dictionary. */
+ public final Locale mLocale;
- private final SharedPreferences mPrefs;
+ private final String mDictName;
- private final ArrayList<PersonalizationDictionaryUpdateSession> mSessions =
- CollectionUtils.newArrayList();
-
- // Should always be false except when we use this class for test
- @UsedForTesting boolean mIsTest = false;
+ /* package */ DecayingExpandableBinaryDictionaryBase(final Context context,
+ final Locale locale, final String dictionaryType, final String dictName) {
+ super(context, dictName, locale, dictionaryType, true);
+ mLocale = locale;
+ mDictName = dictName;
+ if (mLocale != null && mLocale.toString().length() > 1) {
+ reloadDictionaryIfRequired();
+ }
+ }
+ // Creates an instance that uses a given dictionary file for testing.
+ @UsedForTesting
/* package */ DecayingExpandableBinaryDictionaryBase(final Context context,
- final String locale, final SharedPreferences sp, final String dictionaryType,
- final String fileName) {
- super(context, fileName, dictionaryType, true);
+ final Locale locale, final String dictionaryType, final String dictName,
+ final File dictFile) {
+ super(context, dictName, locale, dictionaryType, true, dictFile);
mLocale = locale;
- mFileName = fileName;
- mPrefs = sp;
- if (mLocale != null && mLocale.length() > 1) {
- asyncLoadDictionaryToMemory();
+ mDictName = dictName;
+ if (mLocale != null && mLocale.toString().length() > 1) {
reloadDictionaryIfRequired();
}
}
@Override
public void close() {
- if (!ExpandableBinaryDictionary.ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
- closeBinaryDictionary();
+ if (DBG_DUMP_ON_CLOSE) {
+ dumpAllWordsForDebug();
}
// Flush pending writes.
- // TODO: Remove after this class become to use a dynamic binary dictionary.
- asyncFlashAllBinaryDictionary();
- Settings.writeLastUserHistoryWriteTime(mPrefs, mLocale);
+ asyncFlushBinaryDictionary();
}
@Override
protected Map<String, String> getHeaderAttributeMap() {
HashMap<String, String> attributeMap = new HashMap<String, String>();
- attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE,
- FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_ATTRIBUTE,
FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
- attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFileName);
- attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale);
+ attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_ATTRIBUTE,
+ FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
+ attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mDictName);
+ attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale.toString());
+ attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE,
+ String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
return attributeMap;
}
@@ -113,6 +116,25 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB
return false;
}
+ @Override
+ protected boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) {
+ // This class is using format 4 because it's used by all version 4 dictionaries.
+ // TODO: remove this when all dynamically generated dicts use version 4.
+ return formatVersion == REQUIRED_BINARY_DICTIONARY_VERSION;
+ }
+
+ public void addMultipleDictionaryEntriesToDictionary(
+ final ArrayList<LanguageModelParam> languageModelParams,
+ final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) {
+ if (languageModelParams == null || languageModelParams.isEmpty()) {
+ if (callback != null) {
+ callback.onFinished();
+ }
+ return;
+ }
+ addMultipleDictionaryEntriesDynamically(languageModelParams, callback);
+ }
+
/**
* Pair will be added to the decaying dictionary.
*
@@ -121,70 +143,63 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB
* context, as in beginning of a sentence for example.
* The second word may not be null (a NullPointerException would be thrown).
*/
- public void addToDictionary(final String word0, final String word1, final boolean isValid) {
+ public void addToDictionary(final String word0, final String word1, final boolean isValid,
+ final int timestamp) {
if (word1.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH ||
(word0 != null && word0.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH)) {
return;
}
- final int frequency = ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE ?
- (isValid ? FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS) :
- FREQUENCY_FOR_TYPED;
- addWordDynamically(word1, null /* shortcutTarget */, frequency, 0 /* shortcutFreq */,
- false /* isNotAWord */);
+ final int frequency = isValid ?
+ FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS;
+ addWordDynamically(word1, frequency, null /* shortcutTarget */, 0 /* shortcutFreq */,
+ false /* isNotAWord */, false /* isBlacklisted */, timestamp);
// Do not insert a word as a bigram of itself
if (word1.equals(word0)) {
return;
}
if (null != word0) {
- addBigramDynamically(word0, word1, frequency, isValid);
+ addBigramDynamically(word0, word1, frequency, timestamp);
}
}
- public void cancelAddingUserHistory(final String word0, final String word1) {
- removeBigramDynamically(word0, word1);
- }
-
@Override
protected void loadDictionaryAsync() {
- final int[] profTotalCount = { 0 };
- final String locale = getLocale();
- if (DBG_STRESS_TEST) {
- try {
- Log.w(TAG, "Start stress in loading: " + locale);
- Thread.sleep(15000);
- Log.w(TAG, "End stress in loading");
- } catch (InterruptedException e) {
+ // Never loaded to memory in Java side.
+ }
+
+ @UsedForTesting
+ public void dumpAllWordsForDebug() {
+ runAfterGcForDebug(new Runnable() {
+ @Override
+ public void run() {
+ dumpAllWordsForDebugLocked();
}
- }
- final long last = Settings.readLastUserHistoryWriteTime(mPrefs, locale);
- final long now = System.currentTimeMillis();
- final ExpandableBinaryDictionary dictionary = this;
+ });
+ }
+
+ private void dumpAllWordsForDebugLocked() {
+ Log.d(TAG, "dumpAllWordsForDebug started.");
final OnAddWordListener listener = new OnAddWordListener() {
@Override
public void setUnigram(final String word, final String shortcutTarget,
final int frequency, final int shortcutFreq) {
- if (DBG_SAVE_RESTORE) {
- Log.d(TAG, "load unigram: " + word + "," + frequency);
- }
- addWord(word, shortcutTarget, frequency, shortcutFreq, false /* isNotAWord */);
- ++profTotalCount[0];
+ Log.d(TAG, "load unigram: " + word + "," + frequency);
}
@Override
public void setBigram(final String word0, final String word1, final int frequency) {
if (word0.length() < Constants.DICTIONARY_MAX_WORD_LENGTH
&& word1.length() < Constants.DICTIONARY_MAX_WORD_LENGTH) {
- if (DBG_SAVE_RESTORE) {
- Log.d(TAG, "load bigram: " + word0 + "," + word1 + "," + frequency);
- }
- ++profTotalCount[0];
- addBigram(word0, word1, frequency, last);
+ Log.d(TAG, "load bigram: " + word0 + "," + word1 + "," + frequency);
+ } else {
+ Log.d(TAG, "Skip inserting a too long bigram: " + word0 + "," + word1 + ","
+ + frequency);
}
}
};
// Load the dictionary from binary file
- final File dictFile = new File(mContext.getFilesDir(), mFileName);
+ final File dictFile = new File(mContext.getFilesDir(), mDictName);
final DictDecoder dictDecoder = FormatSpec.getDictDecoder(dictFile,
DictDecoder.USE_BYTEARRAY);
if (dictDecoder == null) {
@@ -198,35 +213,17 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB
UserHistoryDictIOUtils.readDictionaryBinary(dictDecoder, listener);
} catch (IOException e) {
Log.d(TAG, "IOException on opening a bytebuffer", e);
- } finally {
- if (PROFILE_SAVE_RESTORE) {
- final long diff = System.currentTimeMillis() - now;
- Log.d(TAG, "PROF: Load UserHistoryDictionary: "
- + locale + ", " + diff + "ms. load " + profTotalCount[0] + "entries.");
- }
+ } catch (UnsupportedFormatException e) {
+ Log.d(TAG, "Unsupported format, can't read the dictionary", e);
}
}
- protected String getLocale() {
- return mLocale;
- }
-
- public void registerUpdateSession(PersonalizationDictionaryUpdateSession session) {
- session.setPredictionDictionary(this);
- mSessions.add(session);
- session.onDictionaryReady();
- }
-
- public void unRegisterUpdateSession(PersonalizationDictionaryUpdateSession session) {
- mSessions.remove(session);
- }
-
@UsedForTesting
public void clearAndFlushDictionary() {
// Clear the node structure on memory
clear();
// Then flush the cleared state of the dictionary on disk.
- asyncFlashAllBinaryDictionary();
+ asyncFlushBinaryDictionary();
}
/* package */ void decayIfNeeded() {
diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
deleted file mode 100644
index 6f152bb91..000000000
--- a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
+++ /dev/null
@@ -1,190 +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.compat.ActivityManagerCompatUtils;
-import com.android.inputmethod.keyboard.ProximityInfo;
-import com.android.inputmethod.latin.AbstractDictionaryWriter;
-import com.android.inputmethod.latin.ExpandableDictionary;
-import com.android.inputmethod.latin.WordComposer;
-import com.android.inputmethod.latin.ExpandableDictionary.NextWord;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.makedict.DictEncoder;
-import com.android.inputmethod.latin.makedict.FormatSpec;
-import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
-import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils;
-import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.BigramDictionaryInterface;
-import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils;
-import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Map;
-
-// Currently this class is used to implement dynamic prodiction dictionary.
-// TODO: Move to native code.
-public class DynamicPersonalizationDictionaryWriter extends AbstractDictionaryWriter {
- private static final String TAG = DynamicPersonalizationDictionaryWriter.class.getSimpleName();
- /** Maximum number of pairs. Pruning will start when databases goes above this number. */
- public static final int DEFAULT_MAX_HISTORY_BIGRAMS = 10000;
- public static final int LOW_MEMORY_MAX_HISTORY_BIGRAMS = 2000;
-
- /** Any pair being typed or picked */
- private static final int FREQUENCY_FOR_TYPED = 2;
-
- private static final int BINARY_DICT_VERSION = 3;
- private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
- new FormatSpec.FormatOptions(BINARY_DICT_VERSION, true /* supportsDynamicUpdate */);
-
- private final UserHistoryDictionaryBigramList mBigramList =
- new UserHistoryDictionaryBigramList();
- private final ExpandableDictionary mExpandableDictionary;
- private final int mMaxHistoryBigrams;
-
- public DynamicPersonalizationDictionaryWriter(final Context context, final String dictType) {
- super(context, dictType);
- mExpandableDictionary = new ExpandableDictionary(dictType);
- final boolean isLowRamDevice = ActivityManagerCompatUtils.isLowRamDevice(context);
- mMaxHistoryBigrams = isLowRamDevice ?
- LOW_MEMORY_MAX_HISTORY_BIGRAMS : DEFAULT_MAX_HISTORY_BIGRAMS;
- }
-
- @Override
- public void clear() {
- mBigramList.evictAll();
- mExpandableDictionary.clearDictionary();
- }
-
- /**
- * Adds a word unigram to the fusion dictionary. Call updateBinaryDictionary when all changes
- * are done to update the binary dictionary.
- * @param word The word to add.
- * @param shortcutTarget A shortcut target for this word, or null if none.
- * @param frequency The frequency for this unigram.
- * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored
- * if shortcutTarget is null.
- * @param isNotAWord true if this is not a word, i.e. shortcut only.
- */
- @Override
- public void addUnigramWord(final String word, final String shortcutTarget, final int frequency,
- final int shortcutFreq, final boolean isNotAWord) {
- if (mBigramList.size() > mMaxHistoryBigrams * 2) {
- // Too many entries: just stop adding new vocabulary and wait next refresh.
- return;
- }
- mExpandableDictionary.addWord(word, shortcutTarget, frequency, shortcutFreq);
- mBigramList.addBigram(null, word, (byte)frequency);
- }
-
- @Override
- public void addBigramWords(final String word0, final String word1, final int frequency,
- final boolean isValid, final long lastModifiedTime) {
- if (mBigramList.size() > mMaxHistoryBigrams * 2) {
- // Too many entries: just stop adding new vocabulary and wait next refresh.
- return;
- }
- if (lastModifiedTime > 0) {
- mExpandableDictionary.setBigramAndGetFrequency(word0, word1,
- new ForgettingCurveParams(frequency, System.currentTimeMillis(),
- lastModifiedTime));
- mBigramList.addBigram(word0, word1, (byte)frequency);
- } else {
- mExpandableDictionary.setBigramAndGetFrequency(word0, word1,
- new ForgettingCurveParams(isValid));
- mBigramList.addBigram(word0, word1, (byte)frequency);
- }
- }
-
- @Override
- public void removeBigramWords(final String word0, final String word1) {
- if (mBigramList.removeBigram(word0, word1)) {
- mExpandableDictionary.removeBigram(word0, word1);
- }
- }
-
- @Override
- protected void writeDictionary(final DictEncoder dictEncoder,
- final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException {
- UserHistoryDictIOUtils.writeDictionary(dictEncoder,
- new FrequencyProvider(mBigramList, mExpandableDictionary, mMaxHistoryBigrams),
- mBigramList, FORMAT_OPTIONS);
- }
-
- private static class FrequencyProvider implements BigramDictionaryInterface {
- private final UserHistoryDictionaryBigramList mBigramList;
- private final ExpandableDictionary mExpandableDictionary;
- private final int mMaxHistoryBigrams;
-
- public FrequencyProvider(final UserHistoryDictionaryBigramList bigramList,
- final ExpandableDictionary expandableDictionary, final int maxHistoryBigrams) {
- mBigramList = bigramList;
- mExpandableDictionary = expandableDictionary;
- mMaxHistoryBigrams = maxHistoryBigrams;
- }
-
- @Override
- public int getFrequency(final String word0, final String word1) {
- final int freq;
- if (word0 == null) { // unigram
- freq = FREQUENCY_FOR_TYPED;
- } else { // bigram
- final NextWord nw = mExpandableDictionary.getBigramWord(word0, word1);
- if (nw != null) {
- final ForgettingCurveParams forgettingCurveParams = nw.getFcParams();
- final byte prevFc = mBigramList.getBigrams(word0).get(word1);
- final byte fc = forgettingCurveParams.getFc();
- final boolean isValid = forgettingCurveParams.isValid();
- if (prevFc > 0 && prevFc == fc) {
- freq = fc & 0xFF;
- } else if (UserHistoryForgettingCurveUtils.
- needsToSave(fc, isValid, mBigramList.size() <= mMaxHistoryBigrams)) {
- freq = fc & 0xFF;
- } else {
- // Delete this entry
- freq = -1;
- }
- } else {
- // Delete this entry
- freq = -1;
- }
- }
- return freq;
- }
- }
-
- @Override
- public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo,
- boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
- return mExpandableDictionary.getSuggestions(composer, prevWord, proximityInfo,
- blockOffensiveWords, additionalFeaturesOptions);
- }
-
- @Override
- public boolean isValidWord(final String word) {
- return mExpandableDictionary.isValidWord(word);
- }
-
- @UsedForTesting
- public boolean isInBigramListForTests(final String word) {
- // TODO: Use native method to determine whether the word is in dictionary or not
- return mBigramList.containsKey(word) || mBigramList.getBigrams(null).containsKey(word);
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
index f257165cb..9b2b981d5 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
@@ -16,53 +16,37 @@
package com.android.inputmethod.latin.personalization;
+import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.ExpandableBinaryDictionary;
import com.android.inputmethod.latin.utils.CollectionUtils;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Locale;
+
import android.content.Context;
-import android.content.SharedPreferences;
-import java.util.ArrayList;
+public class PersonalizationDictionary extends DecayingExpandableBinaryDictionaryBase {
+ /* package */ static final String NAME = PersonalizationDictionary.class.getSimpleName();
-/**
- * This class is a dictionary for the personalized language model that uses binary dictionary.
- */
-public class PersonalizationDictionary extends ExpandableBinaryDictionary {
- private static final String NAME = "personalization";
private final ArrayList<PersonalizationDictionaryUpdateSession> mSessions =
CollectionUtils.newArrayList();
- /** Locale for which this user history dictionary is storing words */
- private final String mLocale;
-
- public PersonalizationDictionary(final Context context, final String locale,
- final SharedPreferences prefs) {
- // TODO: Make isUpdatable true.
- super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_PERSONALIZATION,
- false /* isUpdatable */);
- mLocale = locale;
- // TODO: Restore last updated time
- loadDictionary();
- }
-
- @Override
- protected void loadDictionaryAsync() {
- // TODO: Implement
- }
-
- @Override
- protected boolean hasContentChanged() {
- return false;
+ /* package */ PersonalizationDictionary(final Context context, final Locale locale) {
+ super(context, locale, Dictionary.TYPE_PERSONALIZATION,
+ getDictNameWithLocale(NAME, locale));
}
- @Override
- protected boolean needsToReloadBeforeWriting() {
- return false;
+ // Creates an instance that uses a given dictionary file for testing.
+ @UsedForTesting
+ public PersonalizationDictionary(final Context context, final Locale locale,
+ final File dictFile) {
+ super(context, locale, Dictionary.TYPE_PERSONALIZATION, getDictNameWithLocale(NAME, locale),
+ dictFile);
}
public void registerUpdateSession(PersonalizationDictionaryUpdateSession session) {
- session.setDictionary(this);
+ session.setPredictionDictionary(this);
mSessions.add(session);
session.onDictionaryReady();
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegistrar.java
index c1833ff14..9a897a582 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegistrar.java
@@ -19,19 +19,22 @@ package com.android.inputmethod.latin.personalization;
import android.content.Context;
import android.content.res.Configuration;
-public class PersonalizationDictionarySessionRegister {
- public static void init(Context context) {
+public class PersonalizationDictionarySessionRegistrar {
+ public static void init(final Context context) {
}
public static void onConfigurationChanged(final Context context, final Configuration conf) {
}
- public static void onUpdateData(Context context, String type) {
+ public static void onUpdateData(final Context context, final String type) {
}
- public static void onRemoveData(Context context, String type) {
+ public static void onRemoveData(final Context context, final String type) {
}
- public static void onDestroy(Context context) {
+ public static void resetAll(final Context context) {
+ }
+
+ public static void close(final Context context) {
}
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
index a86f6e584..61354762b 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
@@ -18,61 +18,37 @@ package com.android.inputmethod.latin.personalization;
import android.content.Context;
+import com.android.inputmethod.latin.BinaryDictionary.LanguageModelParam;
+import com.android.inputmethod.latin.ExpandableBinaryDictionary;
+
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.Locale;
/**
* This class is a session where a data provider can communicate with a personalization
* dictionary.
*/
public abstract class PersonalizationDictionaryUpdateSession {
- /**
- * This class is a parameter for a new unigram or bigram word which will be added
- * to the personalization dictionary.
- */
- public static class PersonalizationLanguageModelParam {
- public final String mWord0;
- public final String mWord1;
- public final boolean mIsValid;
- public final int mFrequency;
- public PersonalizationLanguageModelParam(String word0, String word1, boolean isValid,
- int frequency) {
- mWord0 = word0;
- mWord1 = word1;
- mIsValid = isValid;
- mFrequency = frequency;
- }
- }
-
- // TODO: Use a dynamic binary dictionary instead
public WeakReference<PersonalizationDictionary> mDictionary;
- public WeakReference<DecayingExpandableBinaryDictionaryBase> mPredictionDictionary;
- public final String mSystemLocale;
- public PersonalizationDictionaryUpdateSession(String locale) {
+ public final Locale mSystemLocale;
+
+ public PersonalizationDictionaryUpdateSession(final Locale locale) {
mSystemLocale = locale;
}
public abstract void onDictionaryReady();
- public abstract void onDictionaryClosed(Context context);
+ public abstract void onDictionaryClosed(final Context context);
- public void setDictionary(PersonalizationDictionary dictionary) {
+ public void setPredictionDictionary(final PersonalizationDictionary dictionary) {
mDictionary = new WeakReference<PersonalizationDictionary>(dictionary);
}
- public void setPredictionDictionary(DecayingExpandableBinaryDictionaryBase dictionary) {
- mPredictionDictionary =
- new WeakReference<DecayingExpandableBinaryDictionaryBase>(dictionary);
- }
-
protected PersonalizationDictionary getDictionary() {
return mDictionary == null ? null : mDictionary.get();
}
- protected DecayingExpandableBinaryDictionaryBase getPredictionDictionary() {
- return mPredictionDictionary == null ? null : mPredictionDictionary.get();
- }
-
private void unsetDictionary() {
final PersonalizationDictionary dictionary = getDictionary();
if (dictionary == null) {
@@ -81,48 +57,30 @@ public abstract class PersonalizationDictionaryUpdateSession {
dictionary.unRegisterUpdateSession(this);
}
- private void unsetPredictionDictionary() {
- final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary();
- if (dictionary == null) {
- return;
- }
- dictionary.unRegisterUpdateSession(this);
- }
-
- public void clearAndFlushPredictionDictionary(Context context) {
- final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary();
+ public void clearAndFlushDictionary(final Context context) {
+ final PersonalizationDictionary dictionary = getDictionary();
if (dictionary == null) {
return;
}
dictionary.clearAndFlushDictionary();
}
- public void closeSession(Context context) {
+ public void closeSession(final Context context) {
unsetDictionary();
- unsetPredictionDictionary();
onDictionaryClosed(context);
}
- // TODO: Support multi locale to add bigram
- public void addBigramToPersonalizationDictionary(String word0, String word1, boolean isValid,
- int frequency) {
- final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary();
- if (dictionary == null) {
- return;
- }
- dictionary.addToDictionary(word0, word1, isValid);
- }
-
- // Bulk import
- // TODO: Support multi locale to add bigram
- public void addBigramsToPersonalizationDictionary(
- final ArrayList<PersonalizationLanguageModelParam> lmParams) {
- final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary();
+ // TODO: Support multi locale.
+ public void addMultipleDictionaryEntriesToDictionary(
+ final ArrayList<LanguageModelParam> languageModelParams,
+ final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) {
+ final PersonalizationDictionary dictionary = getDictionary();
if (dictionary == null) {
+ if (callback != null) {
+ callback.onFinished();
+ }
return;
}
- for (final PersonalizationLanguageModelParam lmParam : lmParams) {
- dictionary.addToDictionary(lmParam.mWord0, lmParam.mWord1, lmParam.mIsValid);
- }
+ dictionary.addMultipleDictionaryEntriesToDictionary(languageModelParams, callback);
}
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
index 221ddeeba..38b22e5f6 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
@@ -17,13 +17,15 @@
package com.android.inputmethod.latin.personalization;
import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.FileUtils;
import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
import android.util.Log;
+import java.io.File;
+import java.io.FilenameFilter;
import java.lang.ref.SoftReference;
+import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
public class PersonalizationHelper {
@@ -31,21 +33,16 @@ public class PersonalizationHelper {
private static final boolean DEBUG = false;
private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>
sLangUserHistoryDictCache = CollectionUtils.newConcurrentHashMap();
-
private static final ConcurrentHashMap<String, SoftReference<PersonalizationDictionary>>
sLangPersonalizationDictCache = CollectionUtils.newConcurrentHashMap();
- private static final ConcurrentHashMap<String,
- SoftReference<PersonalizationPredictionDictionary>>
- sLangPersonalizationPredictionDictCache =
- CollectionUtils.newConcurrentHashMap();
-
public static UserHistoryDictionary getUserHistoryDictionary(
- final Context context, final String locale, final SharedPreferences sp) {
+ final Context context, final Locale locale) {
+ final String localeStr = locale.toString();
synchronized (sLangUserHistoryDictCache) {
- if (sLangUserHistoryDictCache.containsKey(locale)) {
+ if (sLangUserHistoryDictCache.containsKey(localeStr)) {
final SoftReference<UserHistoryDictionary> ref =
- sLangUserHistoryDictCache.get(locale);
+ sLangUserHistoryDictCache.get(localeStr);
final UserHistoryDictionary dict = ref == null ? null : ref.get();
if (dict != null) {
if (DEBUG) {
@@ -55,8 +52,9 @@ public class PersonalizationHelper {
return dict;
}
}
- final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale, sp);
- sLangUserHistoryDictCache.put(locale, new SoftReference<UserHistoryDictionary>(dict));
+ final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale);
+ sLangUserHistoryDictCache.put(localeStr,
+ new SoftReference<UserHistoryDictionary>(dict));
return dict;
}
}
@@ -74,58 +72,74 @@ public class PersonalizationHelper {
}
public static void registerPersonalizationDictionaryUpdateSession(final Context context,
- final PersonalizationDictionaryUpdateSession session, String locale) {
- final PersonalizationPredictionDictionary predictionDictionary =
- getPersonalizationPredictionDictionary(context, locale,
- PreferenceManager.getDefaultSharedPreferences(context));
- predictionDictionary.registerUpdateSession(session);
- final PersonalizationDictionary dictionary =
- getPersonalizationDictionary(context, locale,
- PreferenceManager.getDefaultSharedPreferences(context));
- dictionary.registerUpdateSession(session);
+ final PersonalizationDictionaryUpdateSession session, final Locale locale) {
+ final PersonalizationDictionary personalizationDictionary =
+ getPersonalizationDictionary(context, locale);
+ personalizationDictionary.registerUpdateSession(session);
}
public static PersonalizationDictionary getPersonalizationDictionary(
- final Context context, final String locale, final SharedPreferences sp) {
+ final Context context, final Locale locale) {
+ final String localeStr = locale.toString();
synchronized (sLangPersonalizationDictCache) {
- if (sLangPersonalizationDictCache.containsKey(locale)) {
+ if (sLangPersonalizationDictCache.containsKey(localeStr)) {
final SoftReference<PersonalizationDictionary> ref =
- sLangPersonalizationDictCache.get(locale);
+ sLangPersonalizationDictCache.get(localeStr);
final PersonalizationDictionary dict = ref == null ? null : ref.get();
if (dict != null) {
if (DEBUG) {
- Log.w(TAG, "Use cached PersonalizationDictCache for " + locale);
+ Log.w(TAG, "Use cached PersonalizationDictionary for " + locale);
}
return dict;
}
}
- final PersonalizationDictionary dict =
- new PersonalizationDictionary(context, locale, sp);
+ final PersonalizationDictionary dict = new PersonalizationDictionary(context, locale);
sLangPersonalizationDictCache.put(
- locale, new SoftReference<PersonalizationDictionary>(dict));
+ localeStr, new SoftReference<PersonalizationDictionary>(dict));
return dict;
}
}
- public static PersonalizationPredictionDictionary getPersonalizationPredictionDictionary(
- final Context context, final String locale, final SharedPreferences sp) {
- synchronized (sLangPersonalizationPredictionDictCache) {
- if (sLangPersonalizationPredictionDictCache.containsKey(locale)) {
- final SoftReference<PersonalizationPredictionDictionary> ref =
- sLangPersonalizationPredictionDictCache.get(locale);
- final PersonalizationPredictionDictionary dict = ref == null ? null : ref.get();
- if (dict != null) {
- if (DEBUG) {
- Log.w(TAG, "Use cached PersonalizationPredictionDictionary for " + locale);
+ public static void removeAllPersonalizedDictionaries(final Context context) {
+ removeAllDictionaries(context, sLangUserHistoryDictCache,
+ UserHistoryDictionary.NAME);
+ removeAllDictionaries(context, sLangPersonalizationDictCache,
+ PersonalizationDictionary.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()) {
+ if (entry.getValue() != null) {
+ final DecayingExpandableBinaryDictionaryBase dict = entry.getValue().get();
+ if (dict != null) {
+ dict.clearAndFlushDictionary();
}
- return dict;
}
}
- final PersonalizationPredictionDictionary dict =
- new PersonalizationPredictionDictionary(context, locale, sp);
- sLangPersonalizationPredictionDictCache.put(
- locale, new SoftReference<PersonalizationPredictionDictionary>(dict));
- return dict;
+ dictionaryMap.clear();
+ if (!FileUtils.deleteFilteredFiles(
+ context.getFilesDir(), new DictFilter(dictNamePrefix))) {
+ Log.e(TAG, "Cannot remove all existing dictionary files. filesDir: "
+ + context.getFilesDir().getAbsolutePath() + ", dictNamePrefix: "
+ + dictNamePrefix);
+ }
+ }
+ }
+
+ private static class DictFilter implements FilenameFilter {
+ private final String mName;
+
+ DictFilter(final String name) {
+ mName = name;
+ }
+
+ @Override
+ public boolean accept(final File dir, final String name) {
+ return name.startsWith(mName);
}
}
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java
deleted file mode 100644
index 432954453..000000000
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java
+++ /dev/null
@@ -1,37 +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 com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.ExpandableBinaryDictionary;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-
-public class PersonalizationPredictionDictionary extends DecayingExpandableBinaryDictionaryBase {
- private static final String NAME = PersonalizationPredictionDictionary.class.getSimpleName();
-
- /* package */ PersonalizationPredictionDictionary(final Context context, final String locale,
- final SharedPreferences sp) {
- super(context, locale, sp, Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA,
- getDictionaryFileName(locale));
- }
-
- private static String getDictionaryFileName(final String locale) {
- return NAME + "." + locale + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
index a60226d7e..c23bc9bc0 100644
--- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
@@ -16,25 +16,33 @@
package com.android.inputmethod.latin.personalization;
+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;
import android.content.Context;
-import android.content.SharedPreferences;
/**
* 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.
*/
public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBase {
- /* package for tests */ static final String NAME =
- UserHistoryDictionary.class.getSimpleName();
- /* package */ UserHistoryDictionary(final Context context, final String locale,
- final SharedPreferences sp) {
- super(context, locale, sp, Dictionary.TYPE_USER_HISTORY, getDictionaryFileName(locale));
+ /* package */ static final String NAME = UserHistoryDictionary.class.getSimpleName();
+ /* package */ UserHistoryDictionary(final Context context, final Locale locale) {
+ super(context, locale, Dictionary.TYPE_USER_HISTORY, getDictNameWithLocale(NAME, locale));
+ }
+
+ // Creates an instance that uses a given dictionary file for testing.
+ @UsedForTesting
+ public UserHistoryDictionary(final Context context, final Locale locale,
+ final File dictFile) {
+ super(context, locale, Dictionary.TYPE_USER_HISTORY, getDictNameWithLocale(NAME, locale),
+ dictFile);
}
- private static String getDictionaryFileName(final String locale) {
- return NAME + "." + locale + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
+ public void cancelAddingUserHistory(final String word0, final String word1) {
+ removeBigramDynamically(word0, word1);
}
}
diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java
index da1fb73fe..29bbed8bd 100644
--- a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java
@@ -39,8 +39,6 @@ public final class DebugSettings extends PreferenceFragment
public static final String PREF_STATISTICS_LOGGING = "enable_logging";
public static final String PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG =
"use_only_personalization_dictionary_for_debug";
- public static final String PREF_BOOST_PERSONALIZATION_DICTIONARY_FOR_DEBUG =
- "boost_personalization_dictionary_for_debug";
private static final String PREF_READ_EXTERNAL_DICTIONARY = "read_external_dictionary";
private static final boolean SHOW_STATISTICS_LOGGING = false;
@@ -112,8 +110,7 @@ public final class DebugSettings extends PreferenceFragment
updateDebugMode();
mServiceNeedsRestart = true;
}
- } else if (key.equals(PREF_FORCE_NON_DISTINCT_MULTITOUCH)
- || key.equals(PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG)) {
+ } else if (key.equals(PREF_FORCE_NON_DISTINCT_MULTITOUCH)) {
mServiceNeedsRestart = true;
}
}
diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java
index df2c6907f..75c7258ae 100644
--- a/java/src/com/android/inputmethod/latin/settings/Settings.java
+++ b/java/src/com/android/inputmethod/latin/settings/Settings.java
@@ -27,12 +27,10 @@ import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
import com.android.inputmethod.latin.InputAttributes;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
-import com.android.inputmethod.latin.utils.LocaleUtils;
import com.android.inputmethod.latin.utils.ResourceUtils;
import com.android.inputmethod.latin.utils.RunInLocale;
import com.android.inputmethod.latin.utils.StringUtils;
-import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.locks.ReentrantLock;
@@ -53,10 +51,9 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold";
public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting";
public static final String PREF_MISC_SETTINGS = "misc_settings";
- public static final String PREF_LAST_USER_DICTIONARY_WRITE_TIME =
- "last_user_dictionary_write_time";
public static final String PREF_ADVANCED_SETTINGS = "pref_advanced_settings";
public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
+ public static final String PREF_KEY_USE_PERSONALIZED_DICTS = "pref_key_use_personalized_dicts";
public static final String PREF_KEY_USE_DOUBLE_SPACE_PERIOD =
"pref_key_use_double_space_period";
public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE =
@@ -104,6 +101,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_EMOJI_CATEGORY_LAST_TYPED_ID = "emoji_category_last_typed_id";
public static final String PREF_LAST_SHOWN_EMOJI_CATEGORY_ID = "last_shown_emoji_category_id";
+ private Context mContext;
private Resources mRes;
private SharedPreferences mPrefs;
private SettingsValues mSettingsValues;
@@ -124,6 +122,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
}
private void onCreate(final Context context) {
+ mContext = context;
mRes = context.getResources();
mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
mPrefs.registerOnSharedPreferenceChangeListener(this);
@@ -143,20 +142,22 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
Log.w(TAG, "onSharedPreferenceChanged called before loadSettings.");
return;
}
- loadSettings(mSettingsValues.mLocale, mSettingsValues.mInputAttributes);
+ loadSettings(mContext, mSettingsValues.mLocale, mSettingsValues.mInputAttributes);
} finally {
mSettingsValuesLock.unlock();
}
}
- public void loadSettings(final Locale locale, final InputAttributes inputAttributes) {
+ public void loadSettings(final Context context, final Locale locale,
+ final InputAttributes inputAttributes) {
mSettingsValuesLock.lock();
+ mContext = context;
try {
final SharedPreferences prefs = mPrefs;
final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
@Override
protected SettingsValues job(final Resources res) {
- return new SettingsValues(prefs, locale, res, inputAttributes);
+ return new SettingsValues(context, prefs, locale, res, inputAttributes);
}
};
mSettingsValues = job.runInLocale(mRes, locale);
@@ -229,16 +230,15 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
res.getBoolean(R.bool.config_default_phrase_gesture_enabled));
}
- public static boolean readFromBuildConfigIfToShowKeyPreviewPopupSettingsOption(
- final Resources res) {
- return res.getBoolean(R.bool.config_enable_show_option_of_key_preview_popup);
+ public static boolean readFromBuildConfigIfToShowKeyPreviewPopupOption(final Resources res) {
+ return res.getBoolean(R.bool.config_enable_show_key_preview_popup_option);
}
public static boolean readKeyPreviewPopupEnabled(final SharedPreferences prefs,
final Resources res) {
final boolean defaultKeyPreviewPopup = res.getBoolean(
R.bool.config_default_key_preview_popup);
- if (!readFromBuildConfigIfToShowKeyPreviewPopupSettingsOption(res)) {
+ if (!readFromBuildConfigIfToShowKeyPreviewPopupOption(res)) {
return defaultKeyPreviewPopup;
}
return prefs.getBoolean(PREF_POPUP_ON, defaultKeyPreviewPopup);
@@ -333,25 +333,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return prefs.getBoolean(DebugSettings.PREF_USABILITY_STUDY_MODE, true);
}
- public static long readLastUserHistoryWriteTime(final SharedPreferences prefs,
- final String locale) {
- final String str = prefs.getString(PREF_LAST_USER_DICTIONARY_WRITE_TIME, "");
- final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(str);
- if (map.containsKey(locale)) {
- return map.get(locale);
- }
- return 0;
- }
-
- public static void writeLastUserHistoryWriteTime(final SharedPreferences prefs,
- final String locale) {
- final String oldStr = prefs.getString(PREF_LAST_USER_DICTIONARY_WRITE_TIME, "");
- final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(oldStr);
- map.put(locale, System.currentTimeMillis());
- final String newStr = LocaleUtils.localeAndTimeHashMapToStr(map);
- prefs.edit().putString(PREF_LAST_USER_DICTIONARY_WRITE_TIME, newStr).apply();
- }
-
public static boolean readUseFullscreenMode(final Resources res) {
return res.getBoolean(R.bool.config_use_fullscreen_mode);
}
@@ -377,21 +358,13 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return prefs.getBoolean(Settings.PREF_KEY_IS_INTERNAL, false);
}
- public static boolean readUseOnlyPersonalizationDictionaryForDebug(
- final SharedPreferences prefs) {
- return prefs.getBoolean(
- DebugSettings.PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG, false);
- }
-
- public static boolean readBoostPersonalizationDictionaryForDebug(
- final SharedPreferences prefs) {
- return prefs.getBoolean(
- DebugSettings.PREF_BOOST_PERSONALIZATION_DICTIONARY_FOR_DEBUG, false);
- }
-
public void writeLastUsedPersonalizationToken(byte[] token) {
- final String tokenStr = StringUtils.byteArrayToHexString(token);
- mPrefs.edit().putString(PREF_LAST_USED_PERSONALIZATION_TOKEN, tokenStr).apply();
+ if (token == null) {
+ mPrefs.edit().remove(PREF_LAST_USED_PERSONALIZATION_TOKEN).apply();
+ } else {
+ final String tokenStr = StringUtils.byteArrayToHexString(token);
+ mPrefs.edit().putString(PREF_LAST_USED_PERSONALIZATION_TOKEN, tokenStr).apply();
+ }
}
public byte[] readLastUsedPersonalizationToken() {
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
index 5c60a7350..67017a43b 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
@@ -48,7 +48,6 @@ import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
import com.android.inputmethod.latin.utils.ApplicationUtils;
import com.android.inputmethod.latin.utils.FeedbackUtils;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
-import com.android.inputmethod.research.ResearchLogger;
import com.android.inputmethodcommon.InputMethodSettingsFragment;
import java.util.TreeSet;
@@ -61,13 +60,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment
DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS
|| Build.VERSION.SDK_INT <= 18 /* Build.VERSION.JELLY_BEAN_MR2 */;
- private CheckBoxPreference mVoiceInputKeyPreference;
- private ListPreference mShowCorrectionSuggestionsPreference;
- private ListPreference mAutoCorrectionThresholdPreference;
- private ListPreference mKeyPreviewPopupDismissDelay;
- // Use bigrams to predict the next word when there is no input for it yet
- private CheckBoxPreference mBigramPrediction;
-
private void setPreferenceEnabled(final String preferenceKey, final boolean enabled) {
final Preference preference = findPreference(preferenceKey);
if (preference != null) {
@@ -75,6 +67,18 @@ public final class SettingsFragment extends InputMethodSettingsFragment
}
}
+ private void updateListPreferenceSummaryToCurrentValue(final String prefKey) {
+ // Because the "%s" summary trick of {@link ListPreference} doesn't work properly before
+ // KitKat, we need to update the summary programmatically.
+ final ListPreference listPreference = (ListPreference)findPreference(prefKey);
+ if (listPreference == null) {
+ return;
+ }
+ final CharSequence entries[] = listPreference.getEntries();
+ final int entryIndex = listPreference.findIndexOfValue(listPreference.getValue());
+ listPreference.setSummary(entryIndex < 0 ? null : entries[entryIndex]);
+ }
+
private static void removePreference(final String preferenceKey, final PreferenceGroup parent) {
if (parent == null) {
return;
@@ -94,7 +98,7 @@ public final class SettingsFragment extends InputMethodSettingsFragment
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.setTitle(
- ApplicationUtils.getAcitivityTitleResId(getActivity(), SettingsActivity.class));
+ ApplicationUtils.getActivityTitleResId(getActivity(), SettingsActivity.class));
}
final Resources res = getResources();
@@ -107,16 +111,9 @@ public final class SettingsFragment extends InputMethodSettingsFragment
SubtypeLocaleUtils.init(context);
AudioAndHapticFeedbackManager.init(context);
- mVoiceInputKeyPreference =
- (CheckBoxPreference) findPreference(Settings.PREF_VOICE_INPUT_KEY);
- mShowCorrectionSuggestionsPreference =
- (ListPreference) findPreference(Settings.PREF_SHOW_SUGGESTIONS_SETTING);
final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
prefs.registerOnSharedPreferenceChangeListener(this);
- mAutoCorrectionThresholdPreference =
- (ListPreference) findPreference(Settings.PREF_AUTO_CORRECTION_THRESHOLD);
- mBigramPrediction = (CheckBoxPreference) findPreference(Settings.PREF_BIGRAM_PREDICTIONS);
ensureConsistencyOfAutoCorrectionSettings();
final PreferenceGroup generalSettings =
@@ -143,12 +140,7 @@ public final class SettingsFragment extends InputMethodSettingsFragment
feedbackSettings.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(final Preference pref) {
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- // Use development-only feedback mechanism
- ResearchLogger.getInstance().presentFeedbackDialogFromSettings();
- } else {
- FeedbackUtils.showFeedbackForm(getActivity());
- }
+ FeedbackUtils.showFeedbackForm(getActivity());
return true;
}
});
@@ -167,7 +159,7 @@ public final class SettingsFragment extends InputMethodSettingsFragment
final boolean showVoiceKeyOption = res.getBoolean(
R.bool.config_enable_show_voice_key_option);
if (!showVoiceKeyOption) {
- generalSettings.removePreference(mVoiceInputKeyPreference);
+ removePreference(Settings.PREF_VOICE_INPUT_KEY, generalSettings);
}
final PreferenceGroup advancedSettings =
@@ -177,26 +169,27 @@ public final class SettingsFragment extends InputMethodSettingsFragment
removePreference(Settings.PREF_VIBRATION_DURATION_SETTINGS, advancedSettings);
}
- mKeyPreviewPopupDismissDelay =
- (ListPreference) findPreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
- if (!Settings.readFromBuildConfigIfToShowKeyPreviewPopupSettingsOption(res)) {
+ if (!Settings.readFromBuildConfigIfToShowKeyPreviewPopupOption(res)) {
removePreference(Settings.PREF_POPUP_ON, generalSettings);
removePreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, advancedSettings);
} else {
+ // TODO: Cleanup this setup.
+ final ListPreference keyPreviewPopupDismissDelay =
+ (ListPreference) findPreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
R.integer.config_key_preview_linger_timeout));
- mKeyPreviewPopupDismissDelay.setEntries(new String[] {
+ keyPreviewPopupDismissDelay.setEntries(new String[] {
res.getString(R.string.key_preview_popup_dismiss_no_delay),
res.getString(R.string.key_preview_popup_dismiss_default_delay),
});
- mKeyPreviewPopupDismissDelay.setEntryValues(new String[] {
+ keyPreviewPopupDismissDelay.setEntryValues(new String[] {
"0",
popupDismissDelayDefaultValue
});
- if (null == mKeyPreviewPopupDismissDelay.getValue()) {
- mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
+ if (null == keyPreviewPopupDismissDelay.getValue()) {
+ keyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
}
- mKeyPreviewPopupDismissDelay.setEnabled(
+ keyPreviewPopupDismissDelay.setEnabled(
Settings.readKeyPreviewPopupEnabled(prefs, res));
}
@@ -243,20 +236,25 @@ public final class SettingsFragment extends InputMethodSettingsFragment
@Override
public void onResume() {
super.onResume();
- final boolean isShortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled();
- if (!isShortcutImeEnabled) {
- getPreferenceScreen().removePreference(mVoiceInputKeyPreference);
- }
final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
+ final Resources res = getResources();
+ 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
+ : res.getText(R.string.voice_input_disabled_summary));
+ }
final CheckBoxPreference showSetupWizardIcon =
(CheckBoxPreference)findPreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON);
if (showSetupWizardIcon != null) {
showSetupWizardIcon.setChecked(Settings.readShowSetupWizardIcon(prefs, getActivity()));
}
- updateShowCorrectionSuggestionsSummary();
- updateKeyPreviewPopupDelaySummary();
- updateColorSchemeSummary(prefs, getResources());
- updateCustomInputStylesSummary();
+ updateListPreferenceSummaryToCurrentValue(Settings.PREF_SHOW_SUGGESTIONS_SETTING);
+ updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+ updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEYBOARD_LAYOUT);
+ updateCustomInputStylesSummary(prefs, res);
}
@Override
@@ -287,50 +285,26 @@ public final class SettingsFragment extends InputMethodSettingsFragment
LauncherIconVisibilityManager.updateSetupWizardIconVisibility(getActivity());
}
ensureConsistencyOfAutoCorrectionSettings();
- updateShowCorrectionSuggestionsSummary();
- updateKeyPreviewPopupDelaySummary();
- updateColorSchemeSummary(prefs, res);
+ updateListPreferenceSummaryToCurrentValue(Settings.PREF_SHOW_SUGGESTIONS_SETTING);
+ updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+ updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEYBOARD_LAYOUT);
refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, getResources());
}
private void ensureConsistencyOfAutoCorrectionSettings() {
final String autoCorrectionOff = getResources().getString(
R.string.auto_correction_threshold_mode_index_off);
- final String currentSetting = mAutoCorrectionThresholdPreference.getValue();
- mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff));
- }
-
- private void updateShowCorrectionSuggestionsSummary() {
- mShowCorrectionSuggestionsPreference.setSummary(
- getResources().getStringArray(R.array.prefs_suggestion_visibilities)
- [mShowCorrectionSuggestionsPreference.findIndexOfValue(
- mShowCorrectionSuggestionsPreference.getValue())]);
+ 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 updateColorSchemeSummary(final SharedPreferences prefs, final Resources res) {
- // Because the "%s" summary trick of {@link ListPreference} doesn't work properly before
- // KitKat, we need to update the summary by code.
- final Preference preference = findPreference(Settings.PREF_KEYBOARD_LAYOUT);
- if (!(preference instanceof ListPreference)) {
- Log.w(TAG, "Can't find Keyboard Color Scheme preference");
- return;
- }
- final ListPreference colorSchemePreference = (ListPreference)preference;
- final int themeIndex = Settings.readKeyboardThemeIndex(prefs, res);
- int entryIndex = colorSchemePreference.findIndexOfValue(Integer.toString(themeIndex));
- if (entryIndex < 0) {
- final int defaultThemeIndex = Settings.resetAndGetDefaultKeyboardThemeIndex(prefs, res);
- entryIndex = colorSchemePreference.findIndexOfValue(
- Integer.toString(defaultThemeIndex));
- }
- colorSchemePreference.setSummary(colorSchemePreference.getEntries()[entryIndex]);
- }
-
- private void updateCustomInputStylesSummary() {
+ private void updateCustomInputStylesSummary(final SharedPreferences prefs,
+ final Resources res) {
final PreferenceScreen customInputStyles =
(PreferenceScreen)findPreference(Settings.PREF_CUSTOM_INPUT_STYLES);
- final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
- final Resources res = getResources();
final String prefSubtype = Settings.readPrefAdditionalSubtypes(prefs, res);
final InputMethodSubtype[] subtypes =
AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtype);
@@ -342,13 +316,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment
customInputStyles.setSummary(styles);
}
- private void updateKeyPreviewPopupDelaySummary() {
- final ListPreference lp = mKeyPreviewPopupDismissDelay;
- final CharSequence[] entries = lp.getEntries();
- if (entries == null || entries.length <= 0) return;
- lp.setSummary(entries[lp.findIndexOfValue(lp.getValue())]);
- }
-
private void refreshEnablingsOfKeypressSoundAndVibrationSettings(
final SharedPreferences sp, final Resources res) {
setPreferenceEnabled(Settings.PREF_VIBRATION_DURATION_SETTINGS,
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index f331c78e5..a07a0cecf 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -16,25 +16,28 @@
package com.android.inputmethod.latin.settings;
+import android.content.Context;
import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.compat.AppWorkaroundsUtils;
import com.android.inputmethod.keyboard.internal.KeySpecParser;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.Dictionary;
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.SuggestedWords;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.utils.AsyncResultHolder;
import com.android.inputmethod.latin.utils.CollectionUtils;
-import com.android.inputmethod.latin.utils.InputTypeUtils;
import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
import java.util.ArrayList;
import java.util.Arrays;
@@ -50,6 +53,7 @@ public final class SettingsValues {
// 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
// From resources:
public final int mDelayUpdateOldSuggestions;
@@ -67,10 +71,11 @@ public final class SettingsValues {
public final boolean mVibrateOn;
public final boolean mSoundOn;
public final boolean mKeyPreviewPopupOn;
- private final boolean mShowsVoiceInputKey;
+ public final boolean mShowsVoiceInputKey;
public final boolean mIncludesOtherImesInLanguageSwitchList;
public final boolean mShowsLanguageSwitchKey;
public final boolean mUseContactsDict;
+ public final boolean mUsePersonalizedDicts;
public final boolean mUseDoubleSpacePeriod;
public final boolean mBlockPotentiallyOffensive;
// Use bigrams to predict the next word when there is no input for it yet
@@ -94,8 +99,9 @@ public final class SettingsValues {
public final float mAutoCorrectionThreshold;
public final boolean mCorrectionEnabled;
public final int mSuggestionVisibility;
- public final boolean mBoostPersonalizationDictionaryForDebug;
public final boolean mUseOnlyPersonalizationDictionaryForDebug;
+ public final int mDisplayOrientation;
+ private final AsyncResultHolder<AppWorkaroundsUtils> mAppWorkarounds;
// Setting values for additional features
public final int[] mAdditionalFeaturesSettingValues =
@@ -104,8 +110,8 @@ public final class SettingsValues {
// Debug settings
public final boolean mIsInternal;
- public SettingsValues(final SharedPreferences prefs, final Locale locale, final Resources res,
- final InputAttributes inputAttributes) {
+ public SettingsValues(final Context context, final SharedPreferences prefs, final Locale locale,
+ final Resources res, final InputAttributes inputAttributes) {
mLocale = locale;
// Get the resources
mDelayUpdateOldSuggestions = res.getInteger(R.integer.config_delay_update_old_suggestions);
@@ -148,6 +154,7 @@ public final class SettingsValues {
Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false);
mShowsLanguageSwitchKey = Settings.readShowsLanguageSwitchKey(prefs);
mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
+ mUsePersonalizedDicts = prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true);
mUseDoubleSpacePeriod = prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true);
mBlockPotentiallyOffensive = Settings.readBlockPotentiallyOffensive(prefs, res);
mAutoCorrectEnabled = Settings.readAutoCorrectEnabled(autoCorrectionThresholdRawValue, res);
@@ -173,10 +180,18 @@ public final class SettingsValues {
AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray(
prefs, mAdditionalFeaturesSettingValues);
mIsInternal = Settings.isInternal(prefs);
- mBoostPersonalizationDictionaryForDebug =
- Settings.readBoostPersonalizationDictionaryForDebug(prefs);
- mUseOnlyPersonalizationDictionaryForDebug =
- Settings.readUseOnlyPersonalizationDictionaryForDebug(prefs);
+ mUseOnlyPersonalizationDictionaryForDebug = prefs.getBoolean(
+ DebugSettings.PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG, false);
+ mDisplayOrientation = res.getConfiguration().orientation;
+ mAppWorkarounds = new AsyncResultHolder<AppWorkaroundsUtils>();
+ final PackageInfo packageInfo = TargetPackageInfoGetterTask.getCachedPackageInfo(
+ mInputAttributes.mTargetApplicationPackageName);
+ if (null != packageInfo) {
+ mAppWorkarounds.set(new AppWorkaroundsUtils(packageInfo));
+ } else {
+ new TargetPackageInfoGetterTask(context, mAppWorkarounds)
+ .execute(mInputAttributes.mTargetApplicationPackageName);
+ }
}
// Only for tests
@@ -206,6 +221,7 @@ public final class SettingsValues {
mIncludesOtherImesInLanguageSwitchList = false;
mShowsLanguageSwitchKey = true;
mUseContactsDict = true;
+ mUsePersonalizedDicts = true;
mUseDoubleSpacePeriod = true;
mBlockPotentiallyOffensive = true;
mAutoCorrectEnabled = true;
@@ -222,8 +238,10 @@ public final class SettingsValues {
mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
mSuggestionVisibility = 0;
mIsInternal = false;
- mBoostPersonalizationDictionaryForDebug = false;
mUseOnlyPersonalizationDictionaryForDebug = false;
+ mDisplayOrientation = Configuration.ORIENTATION_PORTRAIT;
+ mAppWorkarounds = new AsyncResultHolder<AppWorkaroundsUtils>();
+ mAppWorkarounds.set(null);
}
@UsedForTesting
@@ -235,16 +253,15 @@ public final class SettingsValues {
return mInputAttributes.mApplicationSpecifiedCompletionOn;
}
- public boolean isSuggestionsRequested(final int displayOrientation) {
+ public boolean isSuggestionsRequested() {
return mInputAttributes.mIsSettingsSuggestionStripOn
- && (mCorrectionEnabled
- || isSuggestionStripVisibleInOrientation(displayOrientation));
+ && (mCorrectionEnabled || isSuggestionStripVisible());
}
- public boolean isSuggestionStripVisibleInOrientation(final int orientation) {
+ public boolean isSuggestionStripVisible() {
return (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_VALUE)
|| (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
- && orientation == Configuration.ORIENTATION_PORTRAIT);
+ && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
}
public boolean isWordSeparator(final int code) {
@@ -271,13 +288,6 @@ public final class SettingsValues {
return mInputAttributes.mShouldInsertSpacesAutomatically;
}
- public boolean isVoiceKeyEnabled(final EditorInfo editorInfo) {
- final boolean shortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled();
- final int inputType = (editorInfo != null) ? editorInfo.inputType : 0;
- return shortcutImeEnabled && mShowsVoiceInputKey
- && !InputTypeUtils.isPasswordInputType(inputType);
- }
-
public boolean isLanguageSwitchKeyEnabled() {
if (!mShowsLanguageSwitchKey) {
return false;
@@ -294,6 +304,22 @@ public final class SettingsValues {
return mInputAttributes.isSameInputType(editorInfo);
}
+ public boolean hasSameOrientation(final Configuration configuration) {
+ return mDisplayOrientation == configuration.orientation;
+ }
+
+ public boolean isBeforeJellyBean() {
+ final AppWorkaroundsUtils appWorkaroundUtils
+ = mAppWorkarounds.get(null, TIMEOUT_TO_GET_TARGET_PACKAGE);
+ return null == appWorkaroundUtils ? false : appWorkaroundUtils.isBeforeJellyBean();
+ }
+
+ public boolean isBrokenByRecorrection() {
+ final AppWorkaroundsUtils appWorkaroundUtils
+ = mAppWorkarounds.get(null, TIMEOUT_TO_GET_TARGET_PACKAGE);
+ return null == appWorkaroundUtils ? false : appWorkaroundUtils.isBrokenByRecorrection();
+ }
+
// Helper functions to create member values.
private static SuggestedWords createSuggestPuncList(final String[] puncs) {
final ArrayList<SuggestedWordInfo> puncList = CollectionUtils.newArrayList();
@@ -374,16 +400,20 @@ public final class SettingsValues {
return autoCorrectionThreshold;
}
- private static boolean needsToShowVoiceInputKey(SharedPreferences prefs, Resources res) {
- final String voiceModeMain = res.getString(R.string.voice_mode_main);
- final String voiceMode = prefs.getString(Settings.PREF_VOICE_MODE_OBSOLETE, voiceModeMain);
- final boolean showsVoiceInputKey = voiceMode == null || voiceMode.equals(voiceModeMain);
- if (!showsVoiceInputKey) {
- // Migrate settings from PREF_VOICE_MODE_OBSOLETE to PREF_VOICE_INPUT_KEY
- // Set voiceModeMain as a value of obsolete voice mode settings.
- prefs.edit().putString(Settings.PREF_VOICE_MODE_OBSOLETE, voiceModeMain).apply();
- // Disable voice input key.
- prefs.edit().putBoolean(Settings.PREF_VOICE_INPUT_KEY, false).apply();
+ private static boolean needsToShowVoiceInputKey(final SharedPreferences prefs,
+ final Resources res) {
+ if (!prefs.contains(Settings.PREF_VOICE_INPUT_KEY)) {
+ // Migrate preference from {@link Settings#PREF_VOICE_MODE_OBSOLETE} to
+ // {@link Settings#PREF_VOICE_INPUT_KEY}.
+ final String voiceModeMain = res.getString(R.string.voice_mode_main);
+ final String voiceMode = prefs.getString(
+ Settings.PREF_VOICE_MODE_OBSOLETE, voiceModeMain);
+ final boolean shouldShowVoiceInputKey = voiceModeMain.equals(voiceMode);
+ prefs.edit().putBoolean(Settings.PREF_VOICE_INPUT_KEY, shouldShowVoiceInputKey).apply();
+ }
+ // Remove the obsolete preference if exists.
+ if (prefs.contains(Settings.PREF_VOICE_MODE_OBSOLETE)) {
+ prefs.edit().remove(Settings.PREF_VOICE_MODE_OBSOLETE).apply();
}
return prefs.getBoolean(Settings.PREF_VOICE_INPUT_KEY, true);
}
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
index c4a813c24..5072fabd6 100644
--- a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
+++ b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
@@ -38,7 +38,7 @@ import com.android.inputmethod.compat.ViewCompatUtils;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.settings.SettingsActivity;
import com.android.inputmethod.latin.utils.CollectionUtils;
-import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
+import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper;
import java.util.ArrayList;
@@ -74,21 +74,21 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
private SettingsPoolingHandler mHandler;
private static final class SettingsPoolingHandler
- extends StaticInnerHandlerWrapper<SetupWizardActivity> {
+ extends LeakGuardHandlerWrapper<SetupWizardActivity> {
private static final int MSG_POLLING_IME_SETTINGS = 0;
private static final long IME_SETTINGS_POLLING_INTERVAL = 200;
private final InputMethodManager mImmInHandler;
- public SettingsPoolingHandler(final SetupWizardActivity outerInstance,
+ public SettingsPoolingHandler(final SetupWizardActivity ownerInstance,
final InputMethodManager imm) {
- super(outerInstance);
+ super(ownerInstance);
mImmInHandler = imm;
}
@Override
public void handleMessage(final Message msg) {
- final SetupWizardActivity setupWizardActivity = getOuterInstance();
+ final SetupWizardActivity setupWizardActivity = getOwnerInstance();
if (setupWizardActivity == null) {
return;
}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 503b18b1b..dae36f7dd 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -383,6 +383,8 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
new Thread("spellchecker_close_dicts") {
@Override
public void run() {
+ // Contacts dictionary can be closed multiple times here. If the dictionary is
+ // already closed, extra closings are no-ops, so it's safe.
for (DictionaryPool pool : oldPools.values()) {
pool.close();
}
@@ -428,7 +430,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
final String localeStr = locale.toString();
UserBinaryDictionary userDictionary = mUserDictionaries.get(localeStr);
if (null == userDictionary) {
- userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true);
+ userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, locale, true);
mUserDictionaries.put(localeStr, userDictionary);
}
dictionaryCollection.addDictionary(userDictionary);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
index a0aed2829..b7a5a4026 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
@@ -49,6 +49,7 @@ public final class DictionaryPool extends LinkedBlockingQueue<DictAndKeyboard> {
final static ArrayList<SuggestedWordInfo> noSuggestions = CollectionUtils.newArrayList();
private final static DictAndKeyboard dummyDict = new DictAndKeyboard(
new Dictionary(Dictionary.TYPE_MAIN) {
+ // TODO: this dummy dictionary should be a singleton in the Dictionary class.
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
index 999ca775b..186dafd29 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
@@ -39,7 +39,7 @@ public final class SpellCheckerSettingsFragment extends PreferenceFragment {
addPreferencesFromResource(R.xml.spell_checker_settings);
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
- preferenceScreen.setTitle(ApplicationUtils.getAcitivityTitleResId(
+ preferenceScreen.setTitle(ApplicationUtils.getActivityTitleResId(
getActivity(), SpellCheckerSettingsActivity.class));
}
}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index acd47450b..52012c846 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -66,7 +66,8 @@ public final class MoreSuggestions extends Keyboard {
clearKeys();
mDivider = res.getDrawable(R.drawable.more_suggestions_divider);
mDividerWidth = mDivider.getIntrinsicWidth();
- final float padding = res.getDimension(R.dimen.more_suggestions_key_horizontal_padding);
+ final float padding = res.getDimension(
+ R.dimen.config_more_suggestions_key_horizontal_padding);
int row = 0;
int index = fromIndex;
@@ -75,7 +76,7 @@ public final class MoreSuggestions extends Keyboard {
while (index < size) {
final String word = suggestedWords.getWord(index);
// TODO: Should take care of text x-scaling.
- mWidths[index] = (int)(TypefaceUtils.getLabelWidth(word, paint) + padding);
+ mWidths[index] = (int)(TypefaceUtils.getStringWidth(word, paint) + padding);
final int numColumn = index - rowStartIndex + 1;
final int columnWidth =
(maxWidth - mDividerWidth * (numColumn - 1)) / numColumn;
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
index 0ebe37782..549ff0d9d 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
@@ -54,7 +54,7 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView {
public void adjustVerticalCorrectionForModalMode() {
// Set vertical correction to zero (Reset more keys keyboard sliding allowance
- // {@link R#dimen.more_keys_keyboard_slide_allowance}).
+ // {@link R#dimen.config_more_keys_keyboard_slide_allowance}).
mKeyDetector.setKeyboard(getKeyboard(), -getPaddingLeft(), -getPaddingTop());
}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index faa5560e4..f836e61cb 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -119,7 +119,8 @@ final class SuggestionStripLayoutHelper {
mDividerWidth = dividerView.getMeasuredWidth();
final Resources res = wordView.getResources();
- mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
+ mSuggestionsStripHeight = res.getDimensionPixelSize(
+ R.dimen.config_suggestions_strip_height);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripView);
@@ -145,15 +146,17 @@ final class SuggestionStripLayoutHelper {
a.recycle();
mMoreSuggestionsHint = getMoreSuggestionsHint(res,
- res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect);
+ res.getDimension(R.dimen.config_more_suggestions_hint_text_size),
+ mColorAutoCorrect);
mCenterPositionInStrip = mSuggestionsCountInStrip / 2;
// Assuming there are at least three suggestions. Also, note that the suggestions are
// laid out according to script direction, so this is left of the center for LTR scripts
// and right of the center for RTL scripts.
mTypedWordPositionWhenAutocorrect = mCenterPositionInStrip - 1;
mMoreSuggestionsBottomGap = res.getDimensionPixelOffset(
- R.dimen.more_suggestions_bottom_gap);
- mMoreSuggestionsRowHeight = res.getDimensionPixelSize(R.dimen.more_suggestions_row_height);
+ R.dimen.config_more_suggestions_bottom_gap);
+ mMoreSuggestionsRowHeight = res.getDimensionPixelSize(
+ R.dimen.config_more_suggestions_row_height);
final LayoutInflater inflater = LayoutInflater.from(context);
mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
@@ -205,7 +208,7 @@ final class SuggestionStripLayoutHelper {
}
final String word = suggestedWords.getWord(indexInSuggestedWords);
final boolean isAutoCorrect = indexInSuggestedWords == 1
- && suggestedWords.willAutoCorrect();
+ && suggestedWords.mWillAutoCorrect;
final boolean isTypedWordValid = indexInSuggestedWords == 0
&& suggestedWords.mTypedWordValid;
if (!isAutoCorrect && !isTypedWordValid) {
@@ -229,7 +232,7 @@ final class SuggestionStripLayoutHelper {
final SuggestedWords suggestedWords) {
final int indexToDisplayMostImportantSuggestion;
final int indexToDisplaySecondMostImportantSuggestion;
- if (suggestedWords.willAutoCorrect()) {
+ if (suggestedWords.mWillAutoCorrect) {
indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION;
indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD;
} else {
@@ -254,7 +257,7 @@ final class SuggestionStripLayoutHelper {
final boolean isSuggested = (indexInSuggestedWords != SuggestedWords.INDEX_OF_TYPED_WORD);
final int color;
- if (positionInStrip == mCenterPositionInStrip && suggestedWords.willAutoCorrect()) {
+ if (positionInStrip == mCenterPositionInStrip && suggestedWords.mWillAutoCorrect) {
color = mColorAutoCorrect;
} else if (positionInStrip == mCenterPositionInStrip && suggestedWords.mTypedWordValid) {
color = mColorValidTypedWord;
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index 75f17c559..073148a50 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -112,7 +112,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
final Resources res = context.getResources();
mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset(
- R.dimen.more_suggestions_modal_tolerance);
+ R.dimen.config_more_suggestions_modal_tolerance);
mMoreSuggestionsSlidingDetector = new GestureDetector(
context, mMoreSuggestionsSlidingListener);
}
@@ -162,19 +162,19 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
mSuggestionsStrip.removeAllViews();
removeAllViews();
addView(mSuggestionsStrip);
- mMoreSuggestionsView.dismissMoreKeysPanel();
+ dismissMoreSuggestionsPanel();
}
private final MoreSuggestionsListener mMoreSuggestionsListener = new MoreSuggestionsListener() {
@Override
public void onSuggestionSelected(final int index, final SuggestedWordInfo wordInfo) {
mListener.pickSuggestionManually(index, wordInfo);
- mMoreSuggestionsView.dismissMoreKeysPanel();
+ dismissMoreSuggestionsPanel();
}
@Override
public void onCancelInput() {
- mMoreSuggestionsView.dismissMoreKeysPanel();
+ dismissMoreSuggestionsPanel();
}
};
@@ -192,10 +192,18 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
@Override
public void onCancelMoreKeysPanel(final MoreKeysPanel panel) {
- mMoreSuggestionsView.dismissMoreKeysPanel();
+ dismissMoreSuggestionsPanel();
}
};
+ public boolean isShowingMoreSuggestionPanel() {
+ return mMoreSuggestionsView.isShowingInParent();
+ }
+
+ public void dismissMoreSuggestionsPanel() {
+ mMoreSuggestionsView.dismissMoreKeysPanel();
+ }
+
@Override
public boolean onLongClick(final View view) {
AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(
@@ -322,6 +330,6 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- mMoreSuggestionsView.dismissMoreKeysPanel();
+ dismissMoreSuggestionsPanel();
}
}
diff --git a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
index d87f6f3c4..ef1d0f42c 100644
--- a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
@@ -17,21 +17,25 @@
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.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 android.os.Build;
import android.text.TextUtils;
+import android.util.Log;
import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
-import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
import java.util.ArrayList;
+import java.util.Arrays;
public final class AdditionalSubtypeUtils {
+ private static final String TAG = AdditionalSubtypeUtils.class.getSimpleName();
+
private static final InputMethodSubtype[] EMPTY_SUBTYPE_ARRAY = new InputMethodSubtype[0];
private AdditionalSubtypeUtils() {
@@ -43,6 +47,11 @@ public final class AdditionalSubtypeUtils {
}
private static final String LOCALE_AND_LAYOUT_SEPARATOR = ":";
+ private static final int INDEX_OF_LOCALE = 0;
+ private static final int INDEX_OF_KEYBOARD_LAYOUT = 1;
+ private static final int INDEX_OF_EXTRA_VALUE = 2;
+ private static final int LENGTH_WITHOUT_EXTRA_VALUE = (INDEX_OF_KEYBOARD_LAYOUT + 1);
+ private static final int LENGTH_WITH_EXTRA_VALUE = (INDEX_OF_EXTRA_VALUE + 1);
private static final String PREF_SUBTYPE_SEPARATOR = ";";
public static InputMethodSubtype createAdditionalSubtype(final String localeString,
@@ -79,17 +88,6 @@ public final class AdditionalSubtypeUtils {
: basePrefSubtype + LOCALE_AND_LAYOUT_SEPARATOR + extraValue;
}
- public static InputMethodSubtype createAdditionalSubtype(final String prefSubtype) {
- final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR);
- if (elems.length < 2 || elems.length > 3) {
- throw new RuntimeException("Unknown additional subtype specified: " + prefSubtype);
- }
- final String localeString = elems[0];
- final String keyboardLayoutSetName = elems[1];
- final String extraValue = (elems.length == 3) ? elems[2] : null;
- return createAdditionalSubtype(localeString, keyboardLayoutSetName, extraValue);
- }
-
public static InputMethodSubtype[] createAdditionalSubtypesArray(final String prefSubtypes) {
if (TextUtils.isEmpty(prefSubtypes)) {
return EMPTY_SUBTYPE_ARRAY;
@@ -98,7 +96,19 @@ public final class AdditionalSubtypeUtils {
final ArrayList<InputMethodSubtype> subtypesList =
CollectionUtils.newArrayList(prefSubtypeArray.length);
for (final String prefSubtype : prefSubtypeArray) {
- final InputMethodSubtype subtype = createAdditionalSubtype(prefSubtype);
+ final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR);
+ if (elems.length != LENGTH_WITHOUT_EXTRA_VALUE
+ && elems.length != LENGTH_WITH_EXTRA_VALUE) {
+ Log.w(TAG, "Unknown additional subtype specified: " + prefSubtype + " in "
+ + prefSubtypes);
+ continue;
+ }
+ final String localeString = elems[INDEX_OF_LOCALE];
+ final String keyboardLayoutSetName = elems[INDEX_OF_KEYBOARD_LAYOUT];
+ final String extraValue = (elems.length == LENGTH_WITH_EXTRA_VALUE)
+ ? elems[INDEX_OF_EXTRA_VALUE] : null;
+ final InputMethodSubtype subtype = createAdditionalSubtype(
+ localeString, keyboardLayoutSetName, extraValue);
if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) {
// Skip unknown keyboard layout subtype. This may happen when predefined keyboard
// layout has been removed.
@@ -137,31 +147,36 @@ public final class AdditionalSubtypeUtils {
return sb.toString();
}
- private static InputMethodSubtype buildInputMethodSubtype(int nameId, String localeString,
- String layoutExtraValue, String additionalSubtypeExtraValue) {
- // CAVEAT! If you want to change subtypeId after changing the extra values,
- // you must change "getInputMethodSubtypeId". But it will remove the additional keyboard
- // from the current users. So, you should be really careful to change it.
- final int subtypeId = getInputMethodSubtypeId(nameId, localeString, layoutExtraValue,
- additionalSubtypeExtraValue);
+ private static InputMethodSubtype buildInputMethodSubtype(final int nameId,
+ final String localeString, final String layoutExtraValue,
+ final String additionalSubtypeExtraValue) {
+ // To preserve additional subtype settings and user's selection across OS updates, subtype
+ // id shouldn't be changed. New attributes, such as emojiCapable, are carefully excluded
+ // from the calculation of subtype id.
+ final String compatibleExtraValue = StringUtils.joinCommaSplittableText(
+ layoutExtraValue, additionalSubtypeExtraValue);
+ final int compatibleSubtypeId = getInputMethodSubtypeId(localeString, compatibleExtraValue);
final String extraValue;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- extraValue = layoutExtraValue + "," + additionalSubtypeExtraValue
- + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
- + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
+ // Color Emoji is supported from KitKat.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ extraValue = StringUtils.appendToCommaSplittableTextIfNotExists(
+ EMOJI_CAPABLE, compatibleExtraValue);
} else {
- extraValue = layoutExtraValue + "," + additionalSubtypeExtraValue;
+ extraValue = compatibleExtraValue;
}
return InputMethodSubtypeCompatUtils.newInputMethodSubtype(nameId,
R.drawable.ic_ime_switcher_dark, localeString, KEYBOARD_MODE, extraValue,
- false, false, subtypeId);
+ false, false, compatibleSubtypeId);
}
- private static int getInputMethodSubtypeId(int nameId, String localeString,
- String layoutExtraValue, String additionalSubtypeExtraValue) {
- // TODO: Use InputMethodSubtypeBuilder once we use SDK version 19.
- return (new InputMethodSubtype(nameId, R.drawable.ic_ime_switcher_dark,
- localeString, KEYBOARD_MODE, layoutExtraValue + "," + additionalSubtypeExtraValue,
- false, false)).hashCode();
+ private static int getInputMethodSubtypeId(final String localeString, final String extraValue) {
+ // From the compatibility point of view, the calculation of subtype id has been copied from
+ // {@link InputMethodSubtype} of JellyBean MR2.
+ return Arrays.hashCode(new Object[] {
+ localeString,
+ KEYBOARD_MODE,
+ extraValue,
+ false /* isAuxiliary */,
+ false /* overrideImplicitlyEnabledSubtype */ });
}
}
diff --git a/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java
index 08a2a8c5a..7a4150def 100644
--- a/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java
@@ -31,7 +31,7 @@ public final class ApplicationUtils {
// This utility class is not publicly instantiable.
}
- public static int getAcitivityTitleResId(final Context context,
+ public static int getActivityTitleResId(final Context context,
final Class<? extends Activity> cls) {
final ComponentName cn = new ComponentName(context, cls);
try {
@@ -62,4 +62,22 @@ public final class ApplicationUtils {
}
return "";
}
+
+ /**
+ * A utility method to get the application's PackageInfo.versionCode
+ * @return the application's PackageInfo.versionCode
+ */
+ public static int getVersionCode(final Context context) {
+ try {
+ if (context == null) {
+ return 0;
+ }
+ final String packageName = context.getPackageName();
+ final PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ return info.versionCode;
+ } catch (final NameNotFoundException e) {
+ Log.e(TAG, "Could not find version info.", e);
+ }
+ return 0;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
index c2e97a36f..d12aad639 100644
--- a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
+++ b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
@@ -20,7 +20,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
- * This class is a holder of a result of asynchronous computation.
+ * This class is a holder of the result of an asynchronous computation.
*
* @param <E> the type of the result.
*/
@@ -36,9 +36,9 @@ public class AsyncResultHolder<E> {
}
/**
- * Sets the result value to this holder.
+ * Sets the result value of this holder.
*
- * @param result the value which is set.
+ * @param result the value to set.
*/
public void set(final E result) {
synchronized(mLock) {
@@ -54,12 +54,12 @@ public class AsyncResultHolder<E> {
* Causes the current thread to wait unless the value is set or the specified time is elapsed.
*
* @param defaultValue the default value.
- * @param timeOut the time to wait.
- * @return if the result is set until the time limit then the result, otherwise defaultValue.
+ * @param timeOut the maximum time to wait.
+ * @return if the result is set before the time limit then the result, otherwise defaultValue.
*/
public E get(final E defaultValue, final long timeOut) {
try {
- if(mLatch.await(timeOut, TimeUnit.MILLISECONDS)) {
+ if (mLatch.await(timeOut, TimeUnit.MILLISECONDS)) {
return mResult;
} else {
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 066c5fd32..37c173f96 100644
--- a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
@@ -17,16 +17,11 @@
package com.android.inputmethod.latin.utils;
import com.android.inputmethod.latin.BinaryDictionary;
-import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.Suggest;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import android.text.TextUtils;
import android.util.Log;
-import java.util.concurrent.ConcurrentHashMap;
-
public final class AutoCorrectionUtils {
private static final boolean DBG = LatinImeLogger.sDBG;
private static final String TAG = AutoCorrectionUtils.class.getSimpleName();
@@ -36,48 +31,6 @@ public final class AutoCorrectionUtils {
// Purely static class: can't instantiate.
}
- public static boolean isValidWord(final Suggest suggest, final String word,
- final boolean ignoreCase) {
- if (TextUtils.isEmpty(word)) {
- return false;
- }
- final ConcurrentHashMap<String, Dictionary> dictionaries = suggest.getUnigramDictionaries();
- final String lowerCasedWord = word.toLowerCase(suggest.mLocale);
- for (final String key : dictionaries.keySet()) {
- final Dictionary dictionary = dictionaries.get(key);
- // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
- // managing to get null in here. Presumably the language is changing to a language with
- // no main dictionary and the monkey manages to type a whole word before the thread
- // that reads the dictionary is started or something?
- // 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;
- }
-
- public static int getMaxFrequency(final ConcurrentHashMap<String, Dictionary> dictionaries,
- final String word) {
- if (TextUtils.isEmpty(word)) {
- return Dictionary.NOT_A_PROBABILITY;
- }
- int maxFreq = -1;
- for (final String key : dictionaries.keySet()) {
- final Dictionary dictionary = dictionaries.get(key);
- if (null == dictionary) continue;
- final int tempFreq = dictionary.getFrequency(word);
- if (tempFreq >= maxFreq) {
- maxFreq = tempFreq;
- }
- }
- return maxFreq;
- }
-
public static boolean suggestionExceedsAutoCorrectionThreshold(
final SuggestedWordInfo suggestion, final String consideredWord,
final float autoCorrectionThreshold) {
diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
index 021bf0825..3daa63ff4 100644
--- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
@@ -282,10 +282,19 @@ public class DictionaryInfoUtils {
return BinaryDictIOUtils.getDictionaryFileHeaderOrNull(file, 0, file.length());
}
+ /**
+ * Returns information of the dictionary.
+ *
+ * @param fileAddress the asset dictionary file address.
+ * @return information of the specified dictionary.
+ */
private static DictionaryInfo createDictionaryInfoFromFileAddress(
final AssetFileAddress fileAddress) {
final FileHeader header = BinaryDictIOUtils.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();
@@ -328,7 +337,7 @@ public class DictionaryInfoUtils {
// 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;
+ if (dictionaryInfo == null || !dictionaryInfo.mLocale.equals(locale)) continue;
addOrUpdateDictInfo(dictList, dictionaryInfo);
}
}
diff --git a/java/src/com/android/inputmethod/latin/utils/FileUtils.java b/java/src/com/android/inputmethod/latin/utils/FileUtils.java
new file mode 100644
index 000000000..f1106a6c6
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/FileUtils.java
@@ -0,0 +1,54 @@
+/*
+ * 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/JsonUtils.java b/java/src/com/android/inputmethod/latin/utils/JsonUtils.java
new file mode 100644
index 000000000..764ef72ce
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/JsonUtils.java
@@ -0,0 +1,103 @@
+/*
+ * 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 android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class JsonUtils {
+ private static final String TAG = JsonUtils.class.getSimpleName();
+
+ private static final String INTEGER_CLASS_NAME = Integer.class.getSimpleName();
+ private static final String STRING_CLASS_NAME = String.class.getSimpleName();
+
+ private static final String EMPTY_STRING = "";
+
+ public static List<Object> jsonStrToList(final String s) {
+ final ArrayList<Object> list = CollectionUtils.newArrayList();
+ final JsonReader reader = new JsonReader(new StringReader(s));
+ try {
+ reader.beginArray();
+ while (reader.hasNext()) {
+ reader.beginObject();
+ while (reader.hasNext()) {
+ final String name = reader.nextName();
+ if (name.equals(INTEGER_CLASS_NAME)) {
+ list.add(reader.nextInt());
+ } else if (name.equals(STRING_CLASS_NAME)) {
+ list.add(reader.nextString());
+ } else {
+ Log.w(TAG, "Invalid name: " + name);
+ reader.skipValue();
+ }
+ }
+ reader.endObject();
+ }
+ reader.endArray();
+ return list;
+ } catch (final IOException e) {
+ } finally {
+ close(reader);
+ }
+ return Collections.<Object>emptyList();
+ }
+
+ public static String listToJsonStr(final List<Object> list) {
+ if (list == null || list.isEmpty()) {
+ return EMPTY_STRING;
+ }
+ final StringWriter sw = new StringWriter();
+ final JsonWriter writer = new JsonWriter(sw);
+ try {
+ writer.beginArray();
+ for (final Object o : list) {
+ writer.beginObject();
+ if (o instanceof Integer) {
+ writer.name(INTEGER_CLASS_NAME).value((Integer)o);
+ } else if (o instanceof String) {
+ writer.name(STRING_CLASS_NAME).value((String)o);
+ }
+ writer.endObject();
+ }
+ writer.endArray();
+ return sw.toString();
+ } catch (final IOException e) {
+ } finally {
+ close(writer);
+ }
+ return EMPTY_STRING;
+ }
+
+ private static void close(final Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (final IOException e) {
+ // Ignore
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java b/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java
index e958a7e71..d14ba508b 100644
--- a/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java
@@ -35,7 +35,7 @@ public final class LatinImeLoggerUtils {
public static void onSeparator(final int code, final int x, final int y) {
// Helper method to log a single code point separator
// TODO: cache this mapping of a code point to a string in a sparse array in StringUtils
- onSeparator(new String(new int[]{code}, 0, 1), x, y);
+ onSeparator(StringUtils.newSingleCodePointString(code), x, y);
}
public static void onSeparator(final String separator, final int x, final int y) {
diff --git a/java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java b/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java
index 44e5d17b4..8469c87b0 100644
--- a/java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java
+++ b/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java
@@ -21,22 +21,22 @@ import android.os.Looper;
import java.lang.ref.WeakReference;
-public class StaticInnerHandlerWrapper<T> extends Handler {
- private final WeakReference<T> mOuterInstanceRef;
+public class LeakGuardHandlerWrapper<T> extends Handler {
+ private final WeakReference<T> mOwnerInstanceRef;
- public StaticInnerHandlerWrapper(final T outerInstance) {
- this(outerInstance, Looper.myLooper());
+ public LeakGuardHandlerWrapper(final T ownerInstance) {
+ this(ownerInstance, Looper.myLooper());
}
- public StaticInnerHandlerWrapper(final T outerInstance, final Looper looper) {
+ public LeakGuardHandlerWrapper(final T ownerInstance, final Looper looper) {
super(looper);
- if (outerInstance == null) {
- throw new NullPointerException("outerInstance is null");
+ if (ownerInstance == null) {
+ throw new NullPointerException("ownerInstance is null");
}
- mOuterInstanceRef = new WeakReference<T>(outerInstance);
+ mOwnerInstanceRef = new WeakReference<T>(ownerInstance);
}
- public T getOuterInstance() {
- return mOuterInstanceRef.get();
+ 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
index 22045aa38..0c55484b4 100644
--- a/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java
@@ -30,9 +30,6 @@ import java.util.Locale;
* dictionary pack.
*/
public final class LocaleUtils {
- private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap();
- private static final String LOCALE_AND_TIME_STR_SEPARATER = ",";
-
private LocaleUtils() {
// Intentional empty constructor for utility class.
}
@@ -168,12 +165,14 @@ public final class LocaleUtils {
* Creates a locale from a string specification.
*/
public static Locale constructLocaleFromString(final String localeStr) {
- if (localeStr == null)
+ if (localeStr == null) {
return null;
+ }
synchronized (sLocaleCache) {
- if (sLocaleCache.containsKey(localeStr))
- return sLocaleCache.get(localeStr);
- Locale retval = null;
+ Locale retval = sLocaleCache.get(localeStr);
+ if (retval != null) {
+ return retval;
+ }
String[] localeParams = localeStr.split("_", 3);
if (localeParams.length == 1) {
retval = new Locale(localeParams[0]);
@@ -188,38 +187,4 @@ public final class LocaleUtils {
return retval;
}
}
-
- public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) {
- if (TextUtils.isEmpty(str)) {
- return EMPTY_LT_HASH_MAP;
- }
- final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER);
- final int N = ss.length;
- if (N < 2 || N % 2 != 0) {
- return EMPTY_LT_HASH_MAP;
- }
- final HashMap<String, Long> retval = CollectionUtils.newHashMap();
- for (int i = 0; i < N / 2; ++i) {
- final String localeStr = ss[i * 2];
- final long time = Long.valueOf(ss[i * 2 + 1]);
- retval.put(localeStr, time);
- }
- return retval;
- }
-
- public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) {
- if (map == null || map.isEmpty()) {
- return "";
- }
- final StringBuilder builder = new StringBuilder();
- for (String localeStr : map.keySet()) {
- if (builder.length() > 0) {
- builder.append(LOCALE_AND_TIME_STR_SEPARATER);
- }
- final Long time = map.get(localeStr);
- builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER);
- builder.append(String.valueOf(time));
- }
- return builder.toString();
- }
}
diff --git a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
index 201a70d42..b10d08af3 100644
--- a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
+++ b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
@@ -137,6 +137,7 @@ public class PrioritizedSerialExecutor {
public void shutdown() {
synchronized(mLock) {
mIsShutdown = true;
+ mThreadPoolExecutor.shutdown();
}
}
diff --git a/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
index 22c92446a..deb28a08d 100644
--- a/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
@@ -227,19 +227,19 @@ public final class ResourceUtils {
final String keyboardHeightString = getDeviceOverrideValue(res, R.array.keyboard_heights);
final float keyboardHeight;
if (TextUtils.isEmpty(keyboardHeightString)) {
- keyboardHeight = res.getDimension(R.dimen.keyboardHeight);
+ keyboardHeight = res.getDimension(R.dimen.config_default_keyboard_height);
} else {
keyboardHeight = Float.parseFloat(keyboardHeightString) * dm.density;
}
final float maxKeyboardHeight = res.getFraction(
- R.fraction.maxKeyboardHeight, dm.heightPixels, dm.heightPixels);
+ R.fraction.config_max_keyboard_height, dm.heightPixels, dm.heightPixels);
float minKeyboardHeight = res.getFraction(
- R.fraction.minKeyboardHeight, dm.heightPixels, dm.heightPixels);
+ R.fraction.config_min_keyboard_height, dm.heightPixels, dm.heightPixels);
if (minKeyboardHeight < 0.0f) {
// Specified fraction was negative, so it should be calculated against display
// width.
minKeyboardHeight = -res.getFraction(
- R.fraction.minKeyboardHeight, dm.widthPixels, dm.widthPixels);
+ R.fraction.config_min_keyboard_height, dm.widthPixels, dm.widthPixels);
}
// Keyboard height will not exceed maxKeyboardHeight and will not be less than
// minKeyboardHeight.
diff --git a/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java
index b51fd9377..be0955456 100644
--- a/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java
@@ -40,12 +40,17 @@ public final class SpannableStringUtils {
* are out of range in <code>dest</code>.
*/
public static void copyNonParagraphSuggestionSpansFrom(Spanned source, int start, int end,
- Spannable dest, int destoff) {
+ Spannable dest, int destoff) {
Object[] spans = source.getSpans(start, end, SuggestionSpan.class);
for (int i = 0; i < spans.length; i++) {
int fl = source.getSpanFlags(spans[i]);
- if (0 != (fl & Spannable.SPAN_PARAGRAPH)) continue;
+ // We don't care about the PARAGRAPH flag in LatinIME code. However, if this flag
+ // is set, Spannable#setSpan will throw an exception unless the span is on the edge
+ // 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;
int st = source.getSpanStart(spans[i]);
int en = source.getSpanEnd(spans[i]);
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index a36548392..85f44541e 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -16,20 +16,15 @@
package com.android.inputmethod.latin.utils;
+import android.text.TextUtils;
+import android.util.Log;
+
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.settings.SettingsValues;
-import android.text.TextUtils;
-import android.util.JsonReader;
-import android.util.JsonWriter;
-import android.util.Log;
-
import java.io.IOException;
-import java.io.StringReader;
-import java.io.StringWriter;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -39,6 +34,8 @@ public final class StringUtils {
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 StringUtils() {
// This utility class is not publicly instantiable.
}
@@ -80,6 +77,20 @@ public final class StringUtils {
return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT));
}
+ public static String joinCommaSplittableText(final String head, final String tail) {
+ if (TextUtils.isEmpty(head) && TextUtils.isEmpty(tail)) {
+ return EMPTY_STRING;
+ }
+ // Here either head or tail is not null.
+ if (TextUtils.isEmpty(head)) {
+ return tail;
+ }
+ if (TextUtils.isEmpty(tail)) {
+ return head;
+ }
+ return head + SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT + tail;
+ }
+
public static String appendToCommaSplittableTextIfNotExists(final String text,
final String extraValues) {
if (TextUtils.isEmpty(extraValues)) {
@@ -94,7 +105,7 @@ public final class StringUtils {
public static String removeFromCommaSplittableTextIfExists(final String text,
final String extraValues) {
if (TextUtils.isEmpty(extraValues)) {
- return "";
+ return EMPTY_STRING;
}
final String[] elements = extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT);
if (!containsInArray(text, elements)) {
@@ -239,6 +250,24 @@ public final class StringUtils {
return true;
}
+ /**
+ * Returns true if all code points in text are whitespace, false otherwise. Empty is true.
+ */
+ // Interestingly enough, U+00A0 NO-BREAK SPACE and U+200B ZERO-WIDTH SPACE are not considered
+ // whitespace, while EN SPACE, EM SPACE and IDEOGRAPHIC SPACES are.
+ public static boolean containsOnlyWhitespace(final String text) {
+ final int length = text.length();
+ int i = 0;
+ while (i < length) {
+ final int codePoint = text.codePointAt(i);
+ if (!Character.isWhitespace(codePoint)) {
+ return false;
+ }
+ i += Character.charCount(codePoint);
+ }
+ return true;
+ }
+
@UsedForTesting
public static boolean looksValidForDictionaryInsertion(final CharSequence text,
final SettingsValues settings) {
@@ -367,7 +396,7 @@ public final class StringUtils {
return false;
}
- public static boolean isEmptyStringOrWhiteSpaces(String s) {
+ 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))) {
@@ -378,9 +407,9 @@ public final class StringUtils {
}
@UsedForTesting
- public static String byteArrayToHexString(byte[] bytes) {
+ public static String byteArrayToHexString(final byte[] bytes) {
if (bytes == null || bytes.length == 0) {
- return "";
+ return EMPTY_STRING;
}
final StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
@@ -393,7 +422,7 @@ public final class StringUtils {
* Convert hex string to byte array. The string length must be an even number.
*/
@UsedForTesting
- public static byte[] hexStringToByteArray(String hexString) {
+ public static byte[] hexStringToByteArray(final String hexString) {
if (TextUtils.isEmpty(hexString)) {
return null;
}
@@ -409,67 +438,4 @@ public final class StringUtils {
}
return bytes;
}
-
- public static List<Object> jsonStrToList(String s) {
- final ArrayList<Object> retval = CollectionUtils.newArrayList();
- final JsonReader reader = new JsonReader(new StringReader(s));
- try {
- reader.beginArray();
- while(reader.hasNext()) {
- reader.beginObject();
- while (reader.hasNext()) {
- final String name = reader.nextName();
- if (name.equals(Integer.class.getSimpleName())) {
- retval.add(reader.nextInt());
- } else if (name.equals(String.class.getSimpleName())) {
- retval.add(reader.nextString());
- } else {
- Log.w(TAG, "Invalid name: " + name);
- reader.skipValue();
- }
- }
- reader.endObject();
- }
- reader.endArray();
- return retval;
- } catch (IOException e) {
- } finally {
- try {
- reader.close();
- } catch (IOException e) {
- }
- }
- return Collections.<Object>emptyList();
- }
-
- public static String listToJsonStr(List<Object> list) {
- if (list == null || list.isEmpty()) {
- return "";
- }
- final StringWriter sw = new StringWriter();
- final JsonWriter writer = new JsonWriter(sw);
- try {
- writer.beginArray();
- for (final Object o : list) {
- writer.beginObject();
- if (o instanceof Integer) {
- writer.name(Integer.class.getSimpleName()).value((Integer)o);
- } else if (o instanceof String) {
- writer.name(String.class.getSimpleName()).value((String)o);
- }
- writer.endObject();
- }
- writer.endArray();
- return sw.toString();
- } catch (IOException e) {
- } finally {
- try {
- if (writer != null) {
- writer.close();
- }
- } catch (IOException e) {
- }
- }
- return "";
- }
}
diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
index 102a41b4e..fdbe81ab6 100644
--- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
@@ -197,7 +197,9 @@ public final class SubtypeLocaleUtils {
// es_US spanish F Español (EE.UU.) exception
// fr azerty F Français
// fr_CA qwerty F Français (Canada)
+ // fr_CH swiss F Français (Suisse)
// de qwertz F Deutsch
+ // de_CH swiss T Deutsch (Schweiz)
// zz qwerty F No language (QWERTY) in system locale
// fr qwertz T Français (QWERTZ)
// de qwerty T Deutsch (QWERTY)
@@ -298,7 +300,9 @@ public final class SubtypeLocaleUtils {
// es_US spanish F Es Español Español (EE.UU.) exception
// fr azerty F Fr Français Français
// fr_CA qwerty F Fr Français Français (Canada)
+ // fr_CH swiss F Fr Français Français (Suisse)
// de qwertz F De Deutsch Deutsch
+ // de_CH swiss T De Deutsch Deutsch (Schweiz)
// zz qwerty F QWERTY QWERTY
// fr qwertz T Fr Français Français
// de qwerty T De Deutsch Deutsch
diff --git a/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java b/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java
index afbe2ecad..42ea3c959 100644
--- a/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java
+++ b/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java
@@ -22,6 +22,8 @@ import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.util.LruCache;
+import com.android.inputmethod.compat.AppWorkaroundsUtils;
+
public final class TargetPackageInfoGetterTask extends
AsyncTask<String, Void, PackageInfo> {
private static final int MAX_CACHE_ENTRIES = 64; // arbitrary
@@ -37,17 +39,13 @@ public final class TargetPackageInfoGetterTask extends
sCache.remove(packageName);
}
- public interface OnTargetPackageInfoKnownListener {
- public void onTargetPackageInfoKnown(final PackageInfo info);
- }
-
private Context mContext;
- private final OnTargetPackageInfoKnownListener mListener;
+ private final AsyncResultHolder<AppWorkaroundsUtils> mResult;
public TargetPackageInfoGetterTask(final Context context,
- final OnTargetPackageInfoKnownListener listener) {
+ final AsyncResultHolder<AppWorkaroundsUtils> result) {
mContext = context;
- mListener = listener;
+ mResult = result;
}
@Override
@@ -65,6 +63,6 @@ public final class TargetPackageInfoGetterTask extends
@Override
protected void onPostExecute(final PackageInfo info) {
- mListener.onTargetPackageInfoKnown(info);
+ mResult.set(new AppWorkaroundsUtils(info));
}
}
diff --git a/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java b/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java
index 47ea1ea75..087a7f255 100644
--- a/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java
@@ -22,6 +22,9 @@ import android.graphics.Typeface;
import android.util.SparseArray;
public final class TypefaceUtils {
+ private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' };
+ private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' };
+
private TypefaceUtils() {
// This utility class is not publicly instantiable.
}
@@ -31,7 +34,7 @@ public final class TypefaceUtils {
// Working variable for the following method.
private static final Rect sTextHeightBounds = new Rect();
- public static float getCharHeight(final char[] referenceChar, final Paint paint) {
+ private static float getCharHeight(final char[] referenceChar, final Paint paint) {
final int key = getCharGeometryCacheKey(referenceChar[0], paint);
synchronized (sTextHeightCache) {
final Float cachedValue = sTextHeightCache.get(key);
@@ -51,7 +54,7 @@ public final class TypefaceUtils {
// Working variable for the following method.
private static final Rect sTextWidthBounds = new Rect();
- public static float getCharWidth(final char[] referenceChar, final Paint paint) {
+ private static float getCharWidth(final char[] referenceChar, final Paint paint) {
final int key = getCharGeometryCacheKey(referenceChar[0], paint);
synchronized (sTextWidthCache) {
final Float cachedValue = sTextWidthCache.get(key);
@@ -66,11 +69,6 @@ public final class TypefaceUtils {
}
}
- public static float getStringWidth(final String string, final Paint paint) {
- paint.getTextBounds(string, 0, string.length(), sTextWidthBounds);
- return sTextWidthBounds.width();
- }
-
private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) {
final int labelSize = (int)paint.getTextSize();
final Typeface face = paint.getTypeface();
@@ -86,9 +84,25 @@ public final class TypefaceUtils {
}
}
- public static float getLabelWidth(final String label, final Paint paint) {
- final Rect textBounds = new Rect();
- paint.getTextBounds(label, 0, label.length(), textBounds);
- return textBounds.width();
+ public static float getReferenceCharHeight(final Paint paint) {
+ return getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint);
+ }
+
+ public static float getReferenceCharWidth(final Paint paint) {
+ return getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint);
+ }
+
+ public static float getReferenceDigitWidth(final Paint paint) {
+ return getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint);
+ }
+
+ // Working variable for the following method.
+ private static final Rect sStringWidthBounds = new Rect();
+
+ public static float getStringWidth(final String string, final Paint paint) {
+ synchronized (sStringWidthBounds) {
+ paint.getTextBounds(string, 0, string.length(), sStringWidthBounds);
+ return sStringWidthBounds.width();
+ }
}
}
diff --git a/java/src/com/android/inputmethod/latin/utils/UnigramProperty.java b/java/src/com/android/inputmethod/latin/utils/UnigramProperty.java
new file mode 100644
index 000000000..4feee4393
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/UnigramProperty.java
@@ -0,0 +1,82 @@
+/*
+ * 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 com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.util.ArrayList;
+
+// This has information that belong to a unigram. This class has some detailed attributes such as
+// historical information but they have to be checked only for testing purpose.
+@UsedForTesting
+public class UnigramProperty {
+ public final String mCodePoints;
+ public final boolean mIsNotAWord;
+ public final boolean mIsBlacklisted;
+ public final boolean mHasBigrams;
+ public final boolean mHasShortcuts;
+ public final int mProbability;
+ // mTimestamp, mLevel and mCount are historical info. These values are depend on the
+ // implementation in native code; thus, we must not use them and have any assumptions about
+ // them except for tests.
+ public final int mTimestamp;
+ public final int mLevel;
+ public final int mCount;
+ public final ArrayList<WeightedString> mShortcutTargets = CollectionUtils.newArrayList();
+
+ private static int getCodePointCount(final int[] codePoints) {
+ for (int i = 0; i < codePoints.length; i++) {
+ if (codePoints[i] == 0) {
+ return i;
+ }
+ }
+ return codePoints.length;
+ }
+
+ // This represents invalid unigram when the probability is BinaryDictionary.NOT_A_PROBABILITY.
+ public UnigramProperty(final int[] codePoints, final boolean isNotAWord,
+ final boolean isBlacklisted, final boolean hasBigram,
+ final boolean hasShortcuts, final int probability, final int timestamp,
+ final int level, final int count, final ArrayList<int[]> shortcutTargets,
+ final ArrayList<Integer> shortcutProbabilities) {
+ mCodePoints = new String(codePoints, 0 /* offset */, getCodePointCount(codePoints));
+ mIsNotAWord = isNotAWord;
+ mIsBlacklisted = isBlacklisted;
+ mHasBigrams = hasBigram;
+ mHasShortcuts = hasShortcuts;
+ mProbability = probability;
+ mTimestamp = timestamp;
+ mLevel = level;
+ mCount = count;
+ final int shortcutTargetCount = shortcutTargets.size();
+ for (int i = 0; i < shortcutTargetCount; i++) {
+ final int[] shortcutTargetCodePointArray = shortcutTargets.get(i);
+ final String shortcutTargetString = new String(shortcutTargetCodePointArray,
+ 0 /* offset */, getCodePointCount(shortcutTargetCodePointArray));
+ mShortcutTargets.add(
+ new WeightedString(shortcutTargetString, shortcutProbabilities.get(i)));
+ }
+ }
+
+ @UsedForTesting
+ public boolean isValid() {
+ return mProbability != BinaryDictionary.NOT_A_PROBABILITY;
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
index 635afe7cc..db628fe18 100644
--- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
@@ -70,10 +70,11 @@ public final class UserHistoryDictIOUtils {
/**
* Writes dictionary to file.
*/
+ @UsedForTesting
public static void writeDictionary(final DictEncoder dictEncoder,
final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams,
- final FormatOptions formatOptions) {
- final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams);
+ final FormatOptions formatOptions, final HashMap<String, String> options) {
+ final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams, options);
fusionDict.addOptionAttribute(USES_FORGETTING_CURVE_KEY, USES_FORGETTING_CURVE_VALUE);
fusionDict.addOptionAttribute(LAST_UPDATED_TIME_KEY,
String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
@@ -91,11 +92,10 @@ public final class UserHistoryDictIOUtils {
* Constructs a new FusionDictionary from BigramDictionaryInterface.
*/
@UsedForTesting
- static FusionDictionary constructFusionDictionary(
- final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams) {
+ static FusionDictionary constructFusionDictionary(final BigramDictionaryInterface dict,
+ final UserHistoryDictionaryBigramList bigrams, final HashMap<String, String> options) {
final FusionDictionary fusionDict = new FusionDictionary(new PtNodeArray(),
- new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false,
- false));
+ new FusionDictionary.DictionaryOptions(options));
int profTotal = 0;
for (final String word1 : bigrams.keySet()) {
final HashMap<String, Byte> word1Bigrams = bigrams.getBigrams(word1);
diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java
deleted file mode 100644
index 1992b2f5d..000000000
--- a/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java
+++ /dev/null
@@ -1,233 +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 android.util.Log;
-
-import java.util.concurrent.TimeUnit;
-
-public final class UserHistoryForgettingCurveUtils {
- private static final String TAG = UserHistoryForgettingCurveUtils.class.getSimpleName();
- private static final boolean DEBUG = false;
- private static final int DEFAULT_FC_FREQ = 127;
- private static final int BOOSTED_FC_FREQ = 200;
- private static int FC_FREQ_MAX = DEFAULT_FC_FREQ;
- /* package */ static final int COUNT_MAX = 3;
- private static final int FC_LEVEL_MAX = 3;
- /* package */ static final int ELAPSED_TIME_MAX = 15;
- private static final int ELAPSED_TIME_INTERVAL_HOURS = 6;
- private static final long ELAPSED_TIME_INTERVAL_MILLIS =
- TimeUnit.HOURS.toMillis(ELAPSED_TIME_INTERVAL_HOURS);
- private static final int HALF_LIFE_HOURS = 48;
- private static final int MAX_PUSH_ELAPSED = (FC_LEVEL_MAX + 1) * (ELAPSED_TIME_MAX + 1);
-
- public static void boostMaxFreqForDebug() {
- FC_FREQ_MAX = BOOSTED_FC_FREQ;
- }
-
- public static void resetMaxFreqForDebug() {
- FC_FREQ_MAX = DEFAULT_FC_FREQ;
- }
-
- private UserHistoryForgettingCurveUtils() {
- // This utility class is not publicly instantiable.
- }
-
- public static final class ForgettingCurveParams {
- private byte mFc;
- long mLastTouchedTime = 0;
- private final boolean mIsValid;
-
- private void updateLastTouchedTime() {
- mLastTouchedTime = System.currentTimeMillis();
- }
-
- public ForgettingCurveParams(boolean isValid) {
- this(System.currentTimeMillis(), isValid);
- }
-
- private ForgettingCurveParams(long now, boolean isValid) {
- this(pushCount((byte)0, isValid), now, now, isValid);
- }
-
- /** This constructor is called when the user history bigram dictionary is being restored. */
- public ForgettingCurveParams(int fc, long now, long last) {
- // All words with level >= 1 had been saved.
- // Invalid words with level == 0 had been saved.
- // Valid words words with level == 0 had *not* been saved.
- this(fc, now, last, fcToLevel((byte)fc) > 0);
- }
-
- private ForgettingCurveParams(int fc, long now, long last, boolean isValid) {
- mIsValid = isValid;
- mFc = (byte)fc;
- mLastTouchedTime = last;
- updateElapsedTime(now);
- }
-
- public boolean isValid() {
- return mIsValid;
- }
-
- public byte getFc() {
- updateElapsedTime(System.currentTimeMillis());
- return mFc;
- }
-
- public int getFrequency() {
- updateElapsedTime(System.currentTimeMillis());
- return UserHistoryForgettingCurveUtils.fcToFreq(mFc);
- }
-
- public int notifyTypedAgainAndGetFrequency() {
- updateLastTouchedTime();
- // TODO: Check whether this word is valid or not
- mFc = pushCount(mFc, false);
- return UserHistoryForgettingCurveUtils.fcToFreq(mFc);
- }
-
- private void updateElapsedTime(long now) {
- final int elapsedTimeCount =
- (int)((now - mLastTouchedTime) / ELAPSED_TIME_INTERVAL_MILLIS);
- if (elapsedTimeCount <= 0) {
- return;
- }
- if (elapsedTimeCount >= MAX_PUSH_ELAPSED) {
- mLastTouchedTime = now;
- mFc = 0;
- return;
- }
- for (int i = 0; i < elapsedTimeCount; ++i) {
- mLastTouchedTime += ELAPSED_TIME_INTERVAL_MILLIS;
- mFc = pushElapsedTime(mFc);
- }
- }
- }
-
- /* package */ static int fcToElapsedTime(byte fc) {
- return fc & 0x0F;
- }
-
- /* package */ static int fcToCount(byte fc) {
- return (fc >> 4) & 0x03;
- }
-
- /* package */ static int fcToLevel(byte fc) {
- return (fc >> 6) & 0x03;
- }
-
- private static int calcFreq(int elapsedTime, int count, int level) {
- if (level <= 0) {
- // Reserved words, just return -1
- return -1;
- }
- if (count == COUNT_MAX) {
- // Temporary promote because it's frequently typed recently
- ++level;
- }
- final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime));
- final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level));
- return MathUtils.SCORE_TABLE[l - 1][et];
- }
-
- /* pakcage */ static byte calcFc(int elapsedTime, int count, int level) {
- final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime));
- final int c = Math.min(COUNT_MAX, Math.max(0, count));
- final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level));
- return (byte)(et | (c << 4) | (l << 6));
- }
-
- public static int fcToFreq(byte fc) {
- final int elapsedTime = fcToElapsedTime(fc);
- final int count = fcToCount(fc);
- final int level = fcToLevel(fc);
- return calcFreq(elapsedTime, count, level);
- }
-
- public static byte pushElapsedTime(byte fc) {
- int elapsedTime = fcToElapsedTime(fc);
- int count = fcToCount(fc);
- int level = fcToLevel(fc);
- if (elapsedTime >= ELAPSED_TIME_MAX) {
- // Downgrade level
- elapsedTime = 0;
- count = COUNT_MAX;
- --level;
- } else {
- ++elapsedTime;
- }
- return calcFc(elapsedTime, count, level);
- }
-
- public static byte pushCount(byte fc, boolean isValid) {
- final int elapsedTime = fcToElapsedTime(fc);
- int count = fcToCount(fc);
- int level = fcToLevel(fc);
- if ((elapsedTime == 0 && count >= COUNT_MAX) || (isValid && level == 0)) {
- // Upgrade level
- ++level;
- count = 0;
- if (DEBUG) {
- Log.d(TAG, "Upgrade level.");
- }
- } else {
- ++count;
- }
- return calcFc(0, count, level);
- }
-
- // TODO: isValid should be false for a word whose frequency is 0,
- // or that is not in the dictionary.
- /**
- * Check wheather we should save the bigram to the SQL DB or not
- */
- public static boolean needsToSave(byte fc, boolean isValid, boolean addLevel0Bigram) {
- int level = fcToLevel(fc);
- if (level == 0) {
- if (isValid || !addLevel0Bigram) {
- return false;
- }
- }
- final int elapsedTime = fcToElapsedTime(fc);
- return (elapsedTime < ELAPSED_TIME_MAX - 1 || level > 0);
- }
-
- private static final class MathUtils {
- public static final int[][] SCORE_TABLE = new int[FC_LEVEL_MAX][ELAPSED_TIME_MAX + 1];
- static {
- for (int i = 0; i < FC_LEVEL_MAX; ++i) {
- final float initialFreq;
- if (i >= 2) {
- initialFreq = FC_FREQ_MAX;
- } else if (i == 1) {
- initialFreq = FC_FREQ_MAX / 2;
- } else if (i == 0) {
- initialFreq = FC_FREQ_MAX / 4;
- } else {
- continue;
- }
- for (int j = 0; j < ELAPSED_TIME_MAX; ++j) {
- final float elapsedHours = j * ELAPSED_TIME_INTERVAL_HOURS;
- final float freq = initialFreq
- * (float)Math.pow(initialFreq, elapsedHours / HALF_LIFE_HOURS);
- final int intFreq = Math.min(FC_FREQ_MAX, Math.max(0, (int)freq));
- SCORE_TABLE[i][j] = intFreq;
- }
- }
- }
- }
-}