aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/org/kelar/inputmethod/latin/BinaryDictionary.java
diff options
context:
space:
mode:
authorAmin Bandali <bandali@kelar.org>2024-12-16 21:45:41 -0500
committerAmin Bandali <bandali@kelar.org>2025-01-11 14:17:35 -0500
commite9a0e66716dab4dd3184d009d8920de1961efdfa (patch)
tree02dcc096643d74645bf28459c2834c3d4a2ad7f2 /java/src/org/kelar/inputmethod/latin/BinaryDictionary.java
parentfb3b9360d70596d7e921de8bf7d3ca99564a077e (diff)
downloadlatinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.gz
latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.xz
latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.zip
Rename to Kelar Keyboard (org.kelar.inputmethod.latin)
Diffstat (limited to 'java/src/org/kelar/inputmethod/latin/BinaryDictionary.java')
-rw-r--r--java/src/org/kelar/inputmethod/latin/BinaryDictionary.java669
1 files changed, 669 insertions, 0 deletions
diff --git a/java/src/org/kelar/inputmethod/latin/BinaryDictionary.java b/java/src/org/kelar/inputmethod/latin/BinaryDictionary.java
new file mode 100644
index 000000000..661339dd0
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/BinaryDictionary.java
@@ -0,0 +1,669 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.kelar.inputmethod.latin;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import org.kelar.inputmethod.annotations.UsedForTesting;
+import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import org.kelar.inputmethod.latin.common.ComposedData;
+import org.kelar.inputmethod.latin.common.Constants;
+import org.kelar.inputmethod.latin.common.FileUtils;
+import org.kelar.inputmethod.latin.common.InputPointers;
+import org.kelar.inputmethod.latin.common.StringUtils;
+import org.kelar.inputmethod.latin.makedict.DictionaryHeader;
+import org.kelar.inputmethod.latin.makedict.FormatSpec;
+import org.kelar.inputmethod.latin.makedict.FormatSpec.DictionaryOptions;
+import org.kelar.inputmethod.latin.makedict.UnsupportedFormatException;
+import org.kelar.inputmethod.latin.makedict.WordProperty;
+import org.kelar.inputmethod.latin.settings.SettingsValuesForSuggestion;
+import org.kelar.inputmethod.latin.utils.BinaryDictionaryUtils;
+import org.kelar.inputmethod.latin.utils.JniUtils;
+import org.kelar.inputmethod.latin.utils.WordInputEventForPersonalization;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Implements a static, compacted, binary dictionary of standard words.
+ */
+// TODO: All methods which should be locked need to have a suffix "Locked".
+public final class BinaryDictionary extends Dictionary {
+ private static final String TAG = BinaryDictionary.class.getSimpleName();
+
+ // The cutoff returned by native for auto-commit confidence.
+ // Must be equal to CONFIDENCE_TO_AUTO_COMMIT in native/jni/src/defines.h
+ private static final int CONFIDENCE_TO_AUTO_COMMIT = 1000000;
+
+ public static final int DICTIONARY_MAX_WORD_LENGTH = 48;
+ public static final int MAX_PREV_WORD_COUNT_FOR_N_GRAM = 3;
+
+ @UsedForTesting
+ public static final String UNIGRAM_COUNT_QUERY = "UNIGRAM_COUNT";
+ @UsedForTesting
+ public static final String BIGRAM_COUNT_QUERY = "BIGRAM_COUNT";
+ @UsedForTesting
+ public static final String MAX_UNIGRAM_COUNT_QUERY = "MAX_UNIGRAM_COUNT";
+ @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 getWordPropertyNative().
+ private static final int FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT = 5;
+ private static final int FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX = 0;
+ private static final int FORMAT_WORD_PROPERTY_IS_POSSIBLY_OFFENSIVE_INDEX = 1;
+ private static final int FORMAT_WORD_PROPERTY_HAS_NGRAMS_INDEX = 2;
+ private static final int FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX = 3; // DEPRECATED
+ private static final int FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX = 4;
+
+ // Format to get probability and historical info from native side via getWordPropertyNative().
+ public static final int FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT = 4;
+ public static final int FORMAT_WORD_PROPERTY_PROBABILITY_INDEX = 0;
+ public static final int FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX = 1;
+ public static final int FORMAT_WORD_PROPERTY_LEVEL_INDEX = 2;
+ public static final int FORMAT_WORD_PROPERTY_COUNT_INDEX = 3;
+
+ public static final String DICT_FILE_NAME_SUFFIX_FOR_MIGRATION = ".migrate";
+ public static final String DIR_NAME_SUFFIX_FOR_RECORD_MIGRATION = ".migrating";
+
+ private long mNativeDict;
+ private final long mDictSize;
+ private final String mDictFilePath;
+ private final boolean mUseFullEditDistance;
+ private final boolean mIsUpdatable;
+ private boolean mHasUpdated;
+
+ private final SparseArray<DicTraverseSession> mDicTraverseSessions = new SparseArray<>();
+
+ // TODO: There should be a way to remove used DicTraverseSession objects from
+ // {@code mDicTraverseSessions}.
+ private DicTraverseSession getTraverseSession(final int traverseSessionId) {
+ synchronized(mDicTraverseSessions) {
+ DicTraverseSession traverseSession = mDicTraverseSessions.get(traverseSessionId);
+ if (traverseSession == null) {
+ traverseSession = new DicTraverseSession(mLocale, mNativeDict, mDictSize);
+ mDicTraverseSessions.put(traverseSessionId, traverseSession);
+ }
+ return traverseSession;
+ }
+ }
+
+ /**
+ * Constructs binary dictionary using existing dictionary file.
+ * @param filename the name of the file to read through native code.
+ * @param offset the offset of the dictionary data within the file.
+ * @param length the length of the binary data.
+ * @param useFullEditDistance whether to use the full edit distance in suggestions
+ * @param dictType the dictionary type, as a human-readable string
+ * @param isUpdatable whether to open the dictionary file in writable mode.
+ */
+ public BinaryDictionary(final String filename, final long offset, final long length,
+ final boolean useFullEditDistance, final Locale locale, final String dictType,
+ final boolean isUpdatable) {
+ super(dictType, locale);
+ mDictSize = length;
+ mDictFilePath = filename;
+ mIsUpdatable = isUpdatable;
+ mHasUpdated = false;
+ mUseFullEditDistance = useFullEditDistance;
+ loadDictionary(filename, offset, length, isUpdatable);
+ }
+
+ /**
+ * Constructs binary dictionary on memory.
+ * @param filename the name of the file used to flush.
+ * @param useFullEditDistance whether to use the full edit distance in suggestions
+ * @param dictType the dictionary type, as a human-readable string
+ * @param formatVersion the format version of the dictionary
+ * @param attributeMap the attributes of the dictionary
+ */
+ public BinaryDictionary(final String filename, final boolean useFullEditDistance,
+ final Locale locale, final String dictType, final long formatVersion,
+ final Map<String, String> attributeMap) {
+ super(dictType, locale);
+ mDictSize = 0;
+ mDictFilePath = filename;
+ // On memory dictionary is always updatable.
+ mIsUpdatable = true;
+ mHasUpdated = false;
+ mUseFullEditDistance = useFullEditDistance;
+ final String[] keyArray = new String[attributeMap.size()];
+ final String[] valueArray = new String[attributeMap.size()];
+ int index = 0;
+ for (final String key : attributeMap.keySet()) {
+ keyArray[index] = key;
+ valueArray[index] = attributeMap.get(key);
+ index++;
+ }
+ mNativeDict = createOnMemoryNative(formatVersion, locale.toString(), keyArray, valueArray);
+ }
+
+
+ static {
+ JniUtils.loadNativeLibrary();
+ }
+
+ private static native long openNative(String sourceDir, long dictOffset, long dictSize,
+ boolean isUpdatable);
+ private static native long createOnMemoryNative(long formatVersion,
+ String locale, String[] attributeKeyStringArray, String[] attributeValueStringArray);
+ private static native void getHeaderInfoNative(long dict, int[] outHeaderSize,
+ int[] outFormatVersion, ArrayList<int[]> outAttributeKeys,
+ ArrayList<int[]> outAttributeValues);
+ private static native boolean flushNative(long dict, String filePath);
+ private static native boolean needsToRunGCNative(long dict, boolean mindsBlockByGC);
+ private static native boolean 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 getMaxProbabilityOfExactMatchesNative(long dict, int[] word);
+ private static native int getNgramProbabilityNative(long dict, int[][] prevWordCodePointArrays,
+ boolean[] isBeginningOfSentenceArray, int[] word);
+ private static native void getWordPropertyNative(long dict, int[] word,
+ boolean isBeginningOfSentence, int[] outCodePoints, boolean[] outFlags,
+ int[] outProbabilityInfo, ArrayList<int[][]> outNgramPrevWordsArray,
+ ArrayList<boolean[]> outNgramPrevWordIsBeginningOfSentenceArray,
+ ArrayList<int[]> outNgramTargets, ArrayList<int[]> outNgramProbabilityInfo,
+ ArrayList<int[]> outShortcutTargets, ArrayList<Integer> outShortcutProbabilities);
+ private static native int getNextWordNative(long dict, int token, int[] outCodePoints,
+ boolean[] outIsBeginningOfSentence);
+ private static native void getSuggestionsNative(long dict, long proximityInfo,
+ long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times,
+ int[] pointerIds, int[] inputCodePoints, int inputSize, int[] suggestOptions,
+ int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray,
+ int prevWordCount, int[] outputSuggestionCount, int[] outputCodePoints,
+ int[] outputScores, int[] outputIndices, int[] outputTypes,
+ int[] outputAutoCommitFirstWordConfidence,
+ float[] inOutWeightOfLangModelVsSpatialModel);
+ private static native boolean addUnigramEntryNative(long dict, int[] word, int probability,
+ int[] shortcutTarget, int shortcutProbability, boolean isBeginningOfSentence,
+ boolean isNotAWord, boolean isPossiblyOffensive, int timestamp);
+ private static native boolean removeUnigramEntryNative(long dict, int[] word);
+ private static native boolean addNgramEntryNative(long dict,
+ int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray,
+ int[] word, int probability, int timestamp);
+ private static native boolean removeNgramEntryNative(long dict,
+ int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray, int[] word);
+ private static native boolean updateEntriesForWordWithNgramContextNative(long dict,
+ int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray,
+ int[] word, boolean isValidWord, int count, int timestamp);
+ private static native int updateEntriesForInputEventsNative(long dict,
+ WordInputEventForPersonalization[] inputEvents, int startIndex);
+ private static native String getPropertyNative(long dict, String query);
+ private static native boolean isCorruptedNative(long dict);
+ private static native boolean migrateNative(long dict, String dictFilePath,
+ long newFormatVersion);
+
+ // TODO: Move native dict into session
+ private void loadDictionary(final String path, final long startOffset,
+ final long length, final boolean isUpdatable) {
+ mHasUpdated = false;
+ mNativeDict = openNative(path, startOffset, length, isUpdatable);
+ }
+
+ // TODO: Check isCorrupted() for main dictionaries.
+ public boolean isCorrupted() {
+ if (!isValidDictionary()) {
+ return false;
+ }
+ if (!isCorruptedNative(mNativeDict)) {
+ return false;
+ }
+ // TODO: Record the corruption.
+ Log.e(TAG, "BinaryDictionary (" + mDictFilePath + ") is corrupted.");
+ Log.e(TAG, "locale: " + mLocale);
+ Log.e(TAG, "dict size: " + mDictSize);
+ Log.e(TAG, "updatable: " + mIsUpdatable);
+ return true;
+ }
+
+ public DictionaryHeader getHeader() throws UnsupportedFormatException {
+ if (mNativeDict == 0) {
+ return null;
+ }
+ final int[] outHeaderSize = new int[1];
+ final int[] outFormatVersion = new int[1];
+ final ArrayList<int[]> outAttributeKeys = new ArrayList<>();
+ final ArrayList<int[]> outAttributeValues = new ArrayList<>();
+ getHeaderInfoNative(mNativeDict, outHeaderSize, outFormatVersion, outAttributeKeys,
+ outAttributeValues);
+ final HashMap<String, String> attributes = new HashMap<>();
+ for (int i = 0; i < outAttributeKeys.size(); i++) {
+ final String attributeKey = StringUtils.getStringFromNullTerminatedCodePointArray(
+ outAttributeKeys.get(i));
+ final String attributeValue = StringUtils.getStringFromNullTerminatedCodePointArray(
+ outAttributeValues.get(i));
+ attributes.put(attributeKey, attributeValue);
+ }
+ final boolean hasHistoricalInfo = DictionaryHeader.ATTRIBUTE_VALUE_TRUE.equals(
+ attributes.get(DictionaryHeader.HAS_HISTORICAL_INFO_KEY));
+ return new DictionaryHeader(outHeaderSize[0], new DictionaryOptions(attributes),
+ new FormatSpec.FormatOptions(outFormatVersion[0], hasHistoricalInfo));
+ }
+
+ @Override
+ public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+ final NgramContext ngramContext, final long proximityInfoHandle,
+ final SettingsValuesForSuggestion settingsValuesForSuggestion,
+ final int sessionId, final float weightForLocale,
+ final float[] inOutWeightOfLangModelVsSpatialModel) {
+ if (!isValidDictionary()) {
+ return null;
+ }
+ final DicTraverseSession session = getTraverseSession(sessionId);
+ Arrays.fill(session.mInputCodePoints, Constants.NOT_A_CODE);
+ ngramContext.outputToArray(session.mPrevWordCodePointArrays,
+ session.mIsBeginningOfSentenceArray);
+ final InputPointers inputPointers = composedData.mInputPointers;
+ final boolean isGesture = composedData.mIsBatchMode;
+ final int inputSize;
+ if (!isGesture) {
+ inputSize =
+ composedData.copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
+ session.mInputCodePoints);
+ if (inputSize < 0) {
+ return null;
+ }
+ } else {
+ inputSize = inputPointers.getPointerSize();
+ }
+ session.mNativeSuggestOptions.setUseFullEditDistance(mUseFullEditDistance);
+ session.mNativeSuggestOptions.setIsGesture(isGesture);
+ session.mNativeSuggestOptions.setBlockOffensiveWords(
+ settingsValuesForSuggestion.mBlockPotentiallyOffensive);
+ session.mNativeSuggestOptions.setWeightForLocale(weightForLocale);
+ if (inOutWeightOfLangModelVsSpatialModel != null) {
+ session.mInputOutputWeightOfLangModelVsSpatialModel[0] =
+ inOutWeightOfLangModelVsSpatialModel[0];
+ } else {
+ session.mInputOutputWeightOfLangModelVsSpatialModel[0] =
+ Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL;
+ }
+ // TOOD: Pass multiple previous words information for n-gram.
+ getSuggestionsNative(mNativeDict, proximityInfoHandle,
+ getTraverseSession(sessionId).getSession(), inputPointers.getXCoordinates(),
+ inputPointers.getYCoordinates(), inputPointers.getTimes(),
+ inputPointers.getPointerIds(), session.mInputCodePoints, inputSize,
+ session.mNativeSuggestOptions.getOptions(), session.mPrevWordCodePointArrays,
+ session.mIsBeginningOfSentenceArray, ngramContext.getPrevWordCount(),
+ session.mOutputSuggestionCount, session.mOutputCodePoints, session.mOutputScores,
+ session.mSpaceIndices, session.mOutputTypes,
+ session.mOutputAutoCommitFirstWordConfidence,
+ session.mInputOutputWeightOfLangModelVsSpatialModel);
+ if (inOutWeightOfLangModelVsSpatialModel != null) {
+ inOutWeightOfLangModelVsSpatialModel[0] =
+ session.mInputOutputWeightOfLangModelVsSpatialModel[0];
+ }
+ final int count = session.mOutputSuggestionCount[0];
+ final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>();
+ for (int j = 0; j < count; ++j) {
+ final int start = j * DICTIONARY_MAX_WORD_LENGTH;
+ int len = 0;
+ while (len < DICTIONARY_MAX_WORD_LENGTH
+ && session.mOutputCodePoints[start + len] != 0) {
+ ++len;
+ }
+ if (len > 0) {
+ suggestions.add(new SuggestedWordInfo(
+ new String(session.mOutputCodePoints, start, len),
+ "" /* prevWordsContext */,
+ (int)(session.mOutputScores[j] * weightForLocale),
+ session.mOutputTypes[j],
+ this /* sourceDict */,
+ session.mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */,
+ session.mOutputAutoCommitFirstWordConfidence[0]));
+ }
+ }
+ return suggestions;
+ }
+
+ public boolean isValidDictionary() {
+ return mNativeDict != 0;
+ }
+
+ public int getFormatVersion() {
+ return getFormatVersionNative(mNativeDict);
+ }
+
+ @Override
+ public boolean isInDictionary(final String word) {
+ return getFrequency(word) != NOT_A_PROBABILITY;
+ }
+
+ @Override
+ public int getFrequency(final String word) {
+ if (TextUtils.isEmpty(word)) {
+ return NOT_A_PROBABILITY;
+ }
+ final int[] codePoints = StringUtils.toCodePointArray(word);
+ return getProbabilityNative(mNativeDict, codePoints);
+ }
+
+ @Override
+ public int getMaxFrequencyOfExactMatches(final String word) {
+ if (TextUtils.isEmpty(word)) {
+ return NOT_A_PROBABILITY;
+ }
+ final int[] codePoints = StringUtils.toCodePointArray(word);
+ return getMaxProbabilityOfExactMatchesNative(mNativeDict, codePoints);
+ }
+
+ @UsedForTesting
+ public boolean isValidNgram(final NgramContext ngramContext, final String word) {
+ return getNgramProbability(ngramContext, word) != NOT_A_PROBABILITY;
+ }
+
+ public int getNgramProbability(final NgramContext ngramContext, final String word) {
+ if (!ngramContext.isValid() || TextUtils.isEmpty(word)) {
+ return NOT_A_PROBABILITY;
+ }
+ final int[][] prevWordCodePointArrays = new int[ngramContext.getPrevWordCount()][];
+ final boolean[] isBeginningOfSentenceArray = new boolean[ngramContext.getPrevWordCount()];
+ ngramContext.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
+ final int[] wordCodePoints = StringUtils.toCodePointArray(word);
+ return getNgramProbabilityNative(mNativeDict, prevWordCodePointArrays,
+ isBeginningOfSentenceArray, wordCodePoints);
+ }
+
+ public WordProperty getWordProperty(final String word, final boolean isBeginningOfSentence) {
+ if (word == null) {
+ return null;
+ }
+ final int[] codePoints = StringUtils.toCodePointArray(word);
+ final int[] outCodePoints = new int[DICTIONARY_MAX_WORD_LENGTH];
+ final boolean[] outFlags = new boolean[FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT];
+ final int[] outProbabilityInfo =
+ new int[FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT];
+ final ArrayList<int[][]> outNgramPrevWordsArray = new ArrayList<>();
+ final ArrayList<boolean[]> outNgramPrevWordIsBeginningOfSentenceArray =
+ new ArrayList<>();
+ final ArrayList<int[]> outNgramTargets = new ArrayList<>();
+ final ArrayList<int[]> outNgramProbabilityInfo = new ArrayList<>();
+ final ArrayList<int[]> outShortcutTargets = new ArrayList<>();
+ final ArrayList<Integer> outShortcutProbabilities = new ArrayList<>();
+ getWordPropertyNative(mNativeDict, codePoints, isBeginningOfSentence, outCodePoints,
+ outFlags, outProbabilityInfo, outNgramPrevWordsArray,
+ outNgramPrevWordIsBeginningOfSentenceArray, outNgramTargets,
+ outNgramProbabilityInfo, outShortcutTargets, outShortcutProbabilities);
+ return new WordProperty(codePoints,
+ outFlags[FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX],
+ outFlags[FORMAT_WORD_PROPERTY_IS_POSSIBLY_OFFENSIVE_INDEX],
+ outFlags[FORMAT_WORD_PROPERTY_HAS_NGRAMS_INDEX],
+ outFlags[FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX], outProbabilityInfo,
+ outNgramPrevWordsArray, outNgramPrevWordIsBeginningOfSentenceArray,
+ outNgramTargets, outNgramProbabilityInfo);
+ }
+
+ public static class GetNextWordPropertyResult {
+ public WordProperty mWordProperty;
+ public int mNextToken;
+
+ public GetNextWordPropertyResult(final WordProperty wordProperty, final int nextToken) {
+ mWordProperty = wordProperty;
+ mNextToken = nextToken;
+ }
+ }
+
+ /**
+ * Method to iterate all words in the dictionary for makedict.
+ * If token is 0, this method newly starts iterating the dictionary.
+ */
+ public GetNextWordPropertyResult getNextWordProperty(final int token) {
+ final int[] codePoints = new int[DICTIONARY_MAX_WORD_LENGTH];
+ final boolean[] isBeginningOfSentence = new boolean[1];
+ final int nextToken = getNextWordNative(mNativeDict, token, codePoints,
+ isBeginningOfSentence);
+ final String word = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints);
+ return new GetNextWordPropertyResult(
+ getWordProperty(word, isBeginningOfSentence[0]), nextToken);
+ }
+
+ // Add a unigram entry to binary dictionary with unigram attributes in native code.
+ public boolean addUnigramEntry(
+ final String word, final int probability, final boolean isBeginningOfSentence,
+ final boolean isNotAWord, final boolean isPossiblyOffensive, final int timestamp) {
+ if (word == null || (word.isEmpty() && !isBeginningOfSentence)) {
+ return false;
+ }
+ final int[] codePoints = StringUtils.toCodePointArray(word);
+ if (!addUnigramEntryNative(mNativeDict, codePoints, probability,
+ null /* shortcutTargetCodePoints */, 0 /* shortcutProbability */,
+ isBeginningOfSentence, isNotAWord, isPossiblyOffensive, timestamp)) {
+ return false;
+ }
+ mHasUpdated = true;
+ return true;
+ }
+
+ // Remove a unigram entry from the binary dictionary in native code.
+ public boolean removeUnigramEntry(final String word) {
+ if (TextUtils.isEmpty(word)) {
+ return false;
+ }
+ final int[] codePoints = StringUtils.toCodePointArray(word);
+ if (!removeUnigramEntryNative(mNativeDict, codePoints)) {
+ return false;
+ }
+ mHasUpdated = true;
+ return true;
+ }
+
+ // Add an n-gram entry to the binary dictionary with timestamp in native code.
+ public boolean addNgramEntry(final NgramContext ngramContext, final String word,
+ final int probability, final int timestamp) {
+ if (!ngramContext.isValid() || TextUtils.isEmpty(word)) {
+ return false;
+ }
+ final int[][] prevWordCodePointArrays = new int[ngramContext.getPrevWordCount()][];
+ final boolean[] isBeginningOfSentenceArray = new boolean[ngramContext.getPrevWordCount()];
+ ngramContext.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
+ final int[] wordCodePoints = StringUtils.toCodePointArray(word);
+ if (!addNgramEntryNative(mNativeDict, prevWordCodePointArrays,
+ isBeginningOfSentenceArray, wordCodePoints, probability, timestamp)) {
+ return false;
+ }
+ mHasUpdated = true;
+ return true;
+ }
+
+ // Update entries for the word occurrence with the ngramContext.
+ public boolean updateEntriesForWordWithNgramContext(@Nonnull final NgramContext ngramContext,
+ final String word, final boolean isValidWord, final int count, final int timestamp) {
+ if (TextUtils.isEmpty(word)) {
+ return false;
+ }
+ final int[][] prevWordCodePointArrays = new int[ngramContext.getPrevWordCount()][];
+ final boolean[] isBeginningOfSentenceArray = new boolean[ngramContext.getPrevWordCount()];
+ ngramContext.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
+ final int[] wordCodePoints = StringUtils.toCodePointArray(word);
+ if (!updateEntriesForWordWithNgramContextNative(mNativeDict, prevWordCodePointArrays,
+ isBeginningOfSentenceArray, wordCodePoints, isValidWord, count, timestamp)) {
+ return false;
+ }
+ mHasUpdated = true;
+ return true;
+ }
+
+ @UsedForTesting
+ public void updateEntriesForInputEvents(final WordInputEventForPersonalization[] inputEvents) {
+ if (!isValidDictionary()) {
+ return;
+ }
+ int processedEventCount = 0;
+ while (processedEventCount < inputEvents.length) {
+ if (needsToRunGC(true /* mindsBlockByGC */)) {
+ flushWithGC();
+ }
+ processedEventCount = updateEntriesForInputEventsNative(mNativeDict, inputEvents,
+ processedEventCount);
+ mHasUpdated = true;
+ if (processedEventCount <= 0) {
+ return;
+ }
+ }
+ }
+
+ private void reopen() {
+ close();
+ final File dictFile = new File(mDictFilePath);
+ // 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(), mIsUpdatable);
+ }
+
+ // Flush to dict file if the dictionary has been updated.
+ public boolean flush() {
+ if (!isValidDictionary()) {
+ return false;
+ }
+ if (mHasUpdated) {
+ if (!flushNative(mNativeDict, mDictFilePath)) {
+ return false;
+ }
+ reopen();
+ }
+ return true;
+ }
+
+ // Run GC and flush to dict file if the dictionary has been updated.
+ public boolean flushWithGCIfHasUpdated() {
+ if (mHasUpdated) {
+ return flushWithGC();
+ }
+ return true;
+ }
+
+ // Run GC and flush to dict file.
+ public boolean flushWithGC() {
+ if (!isValidDictionary()) {
+ return false;
+ }
+ if (!flushWithGCNative(mNativeDict, mDictFilePath)) {
+ return false;
+ }
+ reopen();
+ return true;
+ }
+
+ /**
+ * Checks whether GC is needed to run or not.
+ * @param mindsBlockByGC Whether to mind operations blocked by GC. We don't need to care about
+ * the blocking in some situations such as in idle time or just before closing.
+ * @return whether GC is needed to run or not.
+ */
+ public boolean needsToRunGC(final boolean mindsBlockByGC) {
+ if (!isValidDictionary()) {
+ return false;
+ }
+ return needsToRunGCNative(mNativeDict, mindsBlockByGC);
+ }
+
+ public boolean migrateTo(final int newFormatVersion) {
+ if (!isValidDictionary()) {
+ return false;
+ }
+ final File isMigratingDir =
+ new File(mDictFilePath + DIR_NAME_SUFFIX_FOR_RECORD_MIGRATION);
+ if (isMigratingDir.exists()) {
+ isMigratingDir.delete();
+ Log.e(TAG, "Previous migration attempt failed probably due to a crash. "
+ + "Giving up using the old dictionary (" + mDictFilePath + ").");
+ return false;
+ }
+ if (!isMigratingDir.mkdir()) {
+ Log.e(TAG, "Cannot create a dir (" + isMigratingDir.getAbsolutePath()
+ + ") to record migration.");
+ return false;
+ }
+ try {
+ final String tmpDictFilePath = mDictFilePath + DICT_FILE_NAME_SUFFIX_FOR_MIGRATION;
+ if (!migrateNative(mNativeDict, tmpDictFilePath, newFormatVersion)) {
+ return false;
+ }
+ close();
+ final File dictFile = new File(mDictFilePath);
+ final File tmpDictFile = new File(tmpDictFilePath);
+ if (!FileUtils.deleteRecursively(dictFile)) {
+ return false;
+ }
+ if (!BinaryDictionaryUtils.renameDict(tmpDictFile, dictFile)) {
+ return false;
+ }
+ loadDictionary(dictFile.getAbsolutePath(), 0 /* startOffset */,
+ dictFile.length(), mIsUpdatable);
+ return true;
+ } finally {
+ isMigratingDir.delete();
+ }
+ }
+
+ @UsedForTesting
+ public String getPropertyForGettingStats(final String query) {
+ if (!isValidDictionary()) {
+ return "";
+ }
+ return getPropertyNative(mNativeDict, query);
+ }
+
+ @Override
+ public boolean shouldAutoCommit(final SuggestedWordInfo candidate) {
+ return candidate.mAutoCommitFirstWordConfidence > CONFIDENCE_TO_AUTO_COMMIT;
+ }
+
+ @Override
+ public void close() {
+ synchronized (mDicTraverseSessions) {
+ final int sessionsSize = mDicTraverseSessions.size();
+ for (int index = 0; index < sessionsSize; ++index) {
+ final DicTraverseSession traverseSession = mDicTraverseSessions.valueAt(index);
+ if (traverseSession != null) {
+ traverseSession.close();
+ }
+ }
+ mDicTraverseSessions.clear();
+ }
+ closeInternalLocked();
+ }
+
+ private synchronized void closeInternalLocked() {
+ if (mNativeDict != 0) {
+ closeNative(mNativeDict);
+ mNativeDict = 0;
+ }
+ }
+
+ // TODO: Manage BinaryDictionary instances without using WeakReference or something.
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ closeInternalLocked();
+ } finally {
+ super.finalize();
+ }
+ }
+}