BinaryDictionary.java revision 05b1e0d42f9f103516103d4d33e61862c0851e9d
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import android.text.TextUtils;
20import android.util.Log;
21import android.util.SparseArray;
22
23import com.android.inputmethod.annotations.UsedForTesting;
24import com.android.inputmethod.keyboard.ProximityInfo;
25import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
26import com.android.inputmethod.latin.makedict.DictionaryHeader;
27import com.android.inputmethod.latin.makedict.FormatSpec;
28import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions;
29import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
30import com.android.inputmethod.latin.makedict.WordProperty;
31import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
32import com.android.inputmethod.latin.utils.FileUtils;
33import com.android.inputmethod.latin.utils.JniUtils;
34import com.android.inputmethod.latin.utils.LanguageModelParam;
35import com.android.inputmethod.latin.utils.StringUtils;
36
37import java.io.File;
38import java.util.ArrayList;
39import java.util.Arrays;
40import java.util.HashMap;
41import java.util.Locale;
42import java.util.Map;
43
44/**
45 * Implements a static, compacted, binary dictionary of standard words.
46 */
47// TODO: All methods which should be locked need to have a suffix "Locked".
48public final class BinaryDictionary extends Dictionary {
49    private static final String TAG = BinaryDictionary.class.getSimpleName();
50
51    // The cutoff returned by native for auto-commit confidence.
52    // Must be equal to CONFIDENCE_TO_AUTO_COMMIT in native/jni/src/defines.h
53    private static final int CONFIDENCE_TO_AUTO_COMMIT = 1000000;
54
55    @UsedForTesting
56    public static final String UNIGRAM_COUNT_QUERY = "UNIGRAM_COUNT";
57    @UsedForTesting
58    public static final String BIGRAM_COUNT_QUERY = "BIGRAM_COUNT";
59    @UsedForTesting
60    public static final String MAX_UNIGRAM_COUNT_QUERY = "MAX_UNIGRAM_COUNT";
61    @UsedForTesting
62    public static final String MAX_BIGRAM_COUNT_QUERY = "MAX_BIGRAM_COUNT";
63
64    public static final int NOT_A_VALID_TIMESTAMP = -1;
65
66    // Format to get unigram flags from native side via getWordPropertyNative().
67    private static final int FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT = 5;
68    private static final int FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX = 0;
69    private static final int FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX = 1;
70    private static final int FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX = 2;
71    private static final int FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX = 3;
72    private static final int FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX = 4;
73
74    // Format to get probability and historical info from native side via getWordPropertyNative().
75    public static final int FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT = 4;
76    public static final int FORMAT_WORD_PROPERTY_PROBABILITY_INDEX = 0;
77    public static final int FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX = 1;
78    public static final int FORMAT_WORD_PROPERTY_LEVEL_INDEX = 2;
79    public static final int FORMAT_WORD_PROPERTY_COUNT_INDEX = 3;
80
81    public static final String DICT_FILE_NAME_SUFFIX_FOR_MIGRATION = ".migrate";
82
83    private long mNativeDict;
84    private final Locale mLocale;
85    private final long mDictSize;
86    private final String mDictFilePath;
87    private final boolean mUseFullEditDistance;
88    private final boolean mIsUpdatable;
89    private boolean mHasUpdated;
90
91    private final SparseArray<DicTraverseSession> mDicTraverseSessions = new SparseArray<>();
92
93    // TODO: There should be a way to remove used DicTraverseSession objects from
94    // {@code mDicTraverseSessions}.
95    private DicTraverseSession getTraverseSession(final int traverseSessionId) {
96        synchronized(mDicTraverseSessions) {
97            DicTraverseSession traverseSession = mDicTraverseSessions.get(traverseSessionId);
98            if (traverseSession == null) {
99                traverseSession = new DicTraverseSession(mLocale, mNativeDict, mDictSize);
100                mDicTraverseSessions.put(traverseSessionId, traverseSession);
101            }
102            return traverseSession;
103        }
104    }
105
106    /**
107     * Constructs binary dictionary using existing dictionary file.
108     * @param filename the name of the file to read through native code.
109     * @param offset the offset of the dictionary data within the file.
110     * @param length the length of the binary data.
111     * @param useFullEditDistance whether to use the full edit distance in suggestions
112     * @param dictType the dictionary type, as a human-readable string
113     * @param isUpdatable whether to open the dictionary file in writable mode.
114     */
115    public BinaryDictionary(final String filename, final long offset, final long length,
116            final boolean useFullEditDistance, final Locale locale, final String dictType,
117            final boolean isUpdatable) {
118        super(dictType);
119        mLocale = locale;
120        mDictSize = length;
121        mDictFilePath = filename;
122        mIsUpdatable = isUpdatable;
123        mHasUpdated = false;
124        mUseFullEditDistance = useFullEditDistance;
125        loadDictionary(filename, offset, length, isUpdatable);
126    }
127
128    /**
129     * Constructs binary dictionary on memory.
130     * @param filename the name of the file used to flush.
131     * @param useFullEditDistance whether to use the full edit distance in suggestions
132     * @param dictType the dictionary type, as a human-readable string
133     * @param formatVersion the format version of the dictionary
134     * @param attributeMap the attributes of the dictionary
135     */
136    public BinaryDictionary(final String filename, final boolean useFullEditDistance,
137            final Locale locale, final String dictType, final long formatVersion,
138            final Map<String, String> attributeMap) {
139        super(dictType);
140        mLocale = locale;
141        mDictSize = 0;
142        mDictFilePath = filename;
143        // On memory dictionary is always updatable.
144        mIsUpdatable = true;
145        mHasUpdated = false;
146        mUseFullEditDistance = useFullEditDistance;
147        final String[] keyArray = new String[attributeMap.size()];
148        final String[] valueArray = new String[attributeMap.size()];
149        int index = 0;
150        for (final String key : attributeMap.keySet()) {
151            keyArray[index] = key;
152            valueArray[index] = attributeMap.get(key);
153            index++;
154        }
155        mNativeDict = createOnMemoryNative(formatVersion, locale.toString(), keyArray, valueArray);
156    }
157
158
159    static {
160        JniUtils.loadNativeLibrary();
161    }
162
163    private static native long openNative(String sourceDir, long dictOffset, long dictSize,
164            boolean isUpdatable);
165    private static native long createOnMemoryNative(long formatVersion,
166            String locale, String[] attributeKeyStringArray, String[] attributeValueStringArray);
167    private static native void getHeaderInfoNative(long dict, int[] outHeaderSize,
168            int[] outFormatVersion, ArrayList<int[]> outAttributeKeys,
169            ArrayList<int[]> outAttributeValues);
170    private static native boolean flushNative(long dict, String filePath);
171    private static native boolean needsToRunGCNative(long dict, boolean mindsBlockByGC);
172    private static native boolean flushWithGCNative(long dict, String filePath);
173    private static native void closeNative(long dict);
174    private static native int getFormatVersionNative(long dict);
175    private static native int getProbabilityNative(long dict, int[] word);
176    private static native int getMaxProbabilityOfExactMatchesNative(long dict, int[] word);
177    private static native int getNgramProbabilityNative(long dict, int[][] prevWordCodePointArrays,
178            boolean[] isBeginningOfSentenceArray, int[] word);
179    private static native void getWordPropertyNative(long dict, int[] word,
180            boolean isBeginningOfSentence, int[] outCodePoints, boolean[] outFlags,
181            int[] outProbabilityInfo, ArrayList<int[]> outBigramTargets,
182            ArrayList<int[]> outBigramProbabilityInfo, ArrayList<int[]> outShortcutTargets,
183            ArrayList<Integer> outShortcutProbabilities);
184    private static native int getNextWordNative(long dict, int token, int[] outCodePoints,
185            boolean[] outIsBeginningOfSentence);
186    private static native void getSuggestionsNative(long dict, long proximityInfo,
187            long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times,
188            int[] pointerIds, int[] inputCodePoints, int inputSize, int[] suggestOptions,
189            int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray,
190            int[] outputSuggestionCount, int[] outputCodePoints, int[] outputScores,
191            int[] outputIndices, int[] outputTypes, int[] outputAutoCommitFirstWordConfidence,
192            float[] inOutLanguageWeight);
193    private static native boolean addUnigramEntryNative(long dict, int[] word, int probability,
194            int[] shortcutTarget, int shortcutProbability, boolean isBeginningOfSentence,
195            boolean isNotAWord, boolean isBlacklisted, int timestamp);
196    private static native boolean removeUnigramEntryNative(long dict, int[] word);
197    private static native boolean addNgramEntryNative(long dict,
198            int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray,
199            int[] word, int probability, int timestamp);
200    private static native boolean removeNgramEntryNative(long dict,
201            int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray, int[] word);
202    private static native int addMultipleDictionaryEntriesNative(long dict,
203            LanguageModelParam[] languageModelParams, int startIndex);
204    private static native String getPropertyNative(long dict, String query);
205    private static native boolean isCorruptedNative(long dict);
206    private static native boolean migrateNative(long dict, String dictFilePath,
207            long newFormatVersion);
208
209    // TODO: Move native dict into session
210    private final void loadDictionary(final String path, final long startOffset,
211            final long length, final boolean isUpdatable) {
212        mHasUpdated = false;
213        mNativeDict = openNative(path, startOffset, length, isUpdatable);
214    }
215
216    // TODO: Check isCorrupted() for main dictionaries.
217    public boolean isCorrupted() {
218        if (!isValidDictionary()) {
219            return false;
220        }
221        if (!isCorruptedNative(mNativeDict)) {
222            return false;
223        }
224        // TODO: Record the corruption.
225        Log.e(TAG, "BinaryDictionary (" + mDictFilePath + ") is corrupted.");
226        Log.e(TAG, "locale: " + mLocale);
227        Log.e(TAG, "dict size: " + mDictSize);
228        Log.e(TAG, "updatable: " + mIsUpdatable);
229        return true;
230    }
231
232    public DictionaryHeader getHeader() throws UnsupportedFormatException {
233        if (mNativeDict == 0) {
234            return null;
235        }
236        final int[] outHeaderSize = new int[1];
237        final int[] outFormatVersion = new int[1];
238        final ArrayList<int[]> outAttributeKeys = new ArrayList<>();
239        final ArrayList<int[]> outAttributeValues = new ArrayList<>();
240        getHeaderInfoNative(mNativeDict, outHeaderSize, outFormatVersion, outAttributeKeys,
241                outAttributeValues);
242        final HashMap<String, String> attributes = new HashMap<>();
243        for (int i = 0; i < outAttributeKeys.size(); i++) {
244            final String attributeKey = StringUtils.getStringFromNullTerminatedCodePointArray(
245                    outAttributeKeys.get(i));
246            final String attributeValue = StringUtils.getStringFromNullTerminatedCodePointArray(
247                    outAttributeValues.get(i));
248            attributes.put(attributeKey, attributeValue);
249        }
250        final boolean hasHistoricalInfo = DictionaryHeader.ATTRIBUTE_VALUE_TRUE.equals(
251                attributes.get(DictionaryHeader.HAS_HISTORICAL_INFO_KEY));
252        return new DictionaryHeader(outHeaderSize[0], new DictionaryOptions(attributes),
253                new FormatSpec.FormatOptions(outFormatVersion[0], hasHistoricalInfo));
254    }
255
256    @Override
257    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
258            final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
259            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
260            final int sessionId, final float[] inOutLanguageWeight) {
261        if (!isValidDictionary()) {
262            return null;
263        }
264        final DicTraverseSession session = getTraverseSession(sessionId);
265        Arrays.fill(session.mInputCodePoints, Constants.NOT_A_CODE);
266        prevWordsInfo.outputToArray(session.mPrevWordCodePointArrays,
267                session.mIsBeginningOfSentenceArray);
268        final InputPointers inputPointers = composer.getInputPointers();
269        final boolean isGesture = composer.isBatchMode();
270        final int inputSize;
271        if (!isGesture) {
272            inputSize = composer.copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
273                    session.mInputCodePoints);
274            if (inputSize < 0) {
275                return null;
276            }
277        } else {
278            inputSize = inputPointers.getPointerSize();
279        }
280        session.mNativeSuggestOptions.setUseFullEditDistance(mUseFullEditDistance);
281        session.mNativeSuggestOptions.setIsGesture(isGesture);
282        session.mNativeSuggestOptions.setBlockOffensiveWords(blockOffensiveWords);
283        session.mNativeSuggestOptions.setAdditionalFeaturesOptions(additionalFeaturesOptions);
284        if (inOutLanguageWeight != null) {
285            session.mInputOutputLanguageWeight[0] = inOutLanguageWeight[0];
286        } else {
287            session.mInputOutputLanguageWeight[0] = Dictionary.NOT_A_LANGUAGE_WEIGHT;
288        }
289        // TOOD: Pass multiple previous words information for n-gram.
290        getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(),
291                getTraverseSession(sessionId).getSession(), inputPointers.getXCoordinates(),
292                inputPointers.getYCoordinates(), inputPointers.getTimes(),
293                inputPointers.getPointerIds(), session.mInputCodePoints, inputSize,
294                session.mNativeSuggestOptions.getOptions(), session.mPrevWordCodePointArrays,
295                session.mIsBeginningOfSentenceArray, session.mOutputSuggestionCount,
296                session.mOutputCodePoints, session.mOutputScores, session.mSpaceIndices,
297                session.mOutputTypes, session.mOutputAutoCommitFirstWordConfidence,
298                session.mInputOutputLanguageWeight);
299        if (inOutLanguageWeight != null) {
300            inOutLanguageWeight[0] = session.mInputOutputLanguageWeight[0];
301        }
302        final int count = session.mOutputSuggestionCount[0];
303        final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>();
304        for (int j = 0; j < count; ++j) {
305            final int start = j * Constants.DICTIONARY_MAX_WORD_LENGTH;
306            int len = 0;
307            while (len < Constants.DICTIONARY_MAX_WORD_LENGTH
308                    && session.mOutputCodePoints[start + len] != 0) {
309                ++len;
310            }
311            if (len > 0) {
312                suggestions.add(new SuggestedWordInfo(
313                        new String(session.mOutputCodePoints, start, len),
314                        session.mOutputScores[j], session.mOutputTypes[j], this /* sourceDict */,
315                        session.mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */,
316                        session.mOutputAutoCommitFirstWordConfidence[0]));
317            }
318        }
319        return suggestions;
320    }
321
322    public boolean isValidDictionary() {
323        return mNativeDict != 0;
324    }
325
326    public int getFormatVersion() {
327        return getFormatVersionNative(mNativeDict);
328    }
329
330    @Override
331    public boolean isInDictionary(final String word) {
332        return getFrequency(word) != NOT_A_PROBABILITY;
333    }
334
335    @Override
336    public int getFrequency(final String word) {
337        if (TextUtils.isEmpty(word)) return NOT_A_PROBABILITY;
338        int[] codePoints = StringUtils.toCodePointArray(word);
339        return getProbabilityNative(mNativeDict, codePoints);
340    }
341
342    @Override
343    public int getMaxFrequencyOfExactMatches(final String word) {
344        if (TextUtils.isEmpty(word)) return NOT_A_PROBABILITY;
345        int[] codePoints = StringUtils.toCodePointArray(word);
346        return getMaxProbabilityOfExactMatchesNative(mNativeDict, codePoints);
347    }
348
349    @UsedForTesting
350    public boolean isValidNgram(final PrevWordsInfo prevWordsInfo, final String word) {
351        return getNgramProbability(prevWordsInfo, word) != NOT_A_PROBABILITY;
352    }
353
354    public int getNgramProbability(final PrevWordsInfo prevWordsInfo, final String word) {
355        if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) {
356            return NOT_A_PROBABILITY;
357        }
358        final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][];
359        final boolean[] isBeginningOfSentenceArray =
360                new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
361        prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
362        final int[] wordCodePoints = StringUtils.toCodePointArray(word);
363        return getNgramProbabilityNative(mNativeDict, prevWordCodePointArrays,
364                isBeginningOfSentenceArray, wordCodePoints);
365    }
366
367    public WordProperty getWordProperty(final String word, final boolean isBeginningOfSentence) {
368        if (word == null) {
369            return null;
370        }
371        final int[] codePoints = StringUtils.toCodePointArray(word);
372        final int[] outCodePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH];
373        final boolean[] outFlags = new boolean[FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT];
374        final int[] outProbabilityInfo =
375                new int[FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT];
376        final ArrayList<int[]> outBigramTargets = new ArrayList<>();
377        final ArrayList<int[]> outBigramProbabilityInfo = new ArrayList<>();
378        final ArrayList<int[]> outShortcutTargets = new ArrayList<>();
379        final ArrayList<Integer> outShortcutProbabilities = new ArrayList<>();
380        getWordPropertyNative(mNativeDict, codePoints, isBeginningOfSentence, outCodePoints,
381                outFlags, outProbabilityInfo, outBigramTargets, outBigramProbabilityInfo,
382                outShortcutTargets, outShortcutProbabilities);
383        return new WordProperty(codePoints,
384                outFlags[FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX],
385                outFlags[FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX],
386                outFlags[FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX],
387                outFlags[FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX],
388                outFlags[FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX], outProbabilityInfo,
389                outBigramTargets, outBigramProbabilityInfo, outShortcutTargets,
390                outShortcutProbabilities);
391    }
392
393    public static class GetNextWordPropertyResult {
394        public WordProperty mWordProperty;
395        public int mNextToken;
396
397        public GetNextWordPropertyResult(final WordProperty wordProperty, final int nextToken) {
398            mWordProperty = wordProperty;
399            mNextToken = nextToken;
400        }
401    }
402
403    /**
404     * Method to iterate all words in the dictionary for makedict.
405     * If token is 0, this method newly starts iterating the dictionary.
406     */
407    public GetNextWordPropertyResult getNextWordProperty(final int token) {
408        final int[] codePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH];
409        final boolean[] isBeginningOfSentence = new boolean[1];
410        final int nextToken = getNextWordNative(mNativeDict, token, codePoints,
411                isBeginningOfSentence);
412        final String word = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints);
413        return new GetNextWordPropertyResult(
414                getWordProperty(word, isBeginningOfSentence[0]), nextToken);
415    }
416
417    // Add a unigram entry to binary dictionary with unigram attributes in native code.
418    public boolean addUnigramEntry(final String word, final int probability,
419            final String shortcutTarget, final int shortcutProbability,
420            final boolean isBeginningOfSentence, final boolean isNotAWord,
421            final boolean isBlacklisted, final int timestamp) {
422        if (word == null || (word.isEmpty() && !isBeginningOfSentence)) {
423            return false;
424        }
425        final int[] codePoints = StringUtils.toCodePointArray(word);
426        final int[] shortcutTargetCodePoints = (shortcutTarget != null) ?
427                StringUtils.toCodePointArray(shortcutTarget) : null;
428        if (!addUnigramEntryNative(mNativeDict, codePoints, probability, shortcutTargetCodePoints,
429                shortcutProbability, isBeginningOfSentence, isNotAWord, isBlacklisted, timestamp)) {
430            return false;
431        }
432        mHasUpdated = true;
433        return true;
434    }
435
436    // Remove a unigram entry from the binary dictionary in native code.
437    public boolean removeUnigramEntry(final String word) {
438        if (TextUtils.isEmpty(word)) {
439            return false;
440        }
441        final int[] codePoints = StringUtils.toCodePointArray(word);
442        if (!removeUnigramEntryNative(mNativeDict, codePoints)) {
443            return false;
444        }
445        mHasUpdated = true;
446        return true;
447    }
448
449    // Add an n-gram entry to the binary dictionary with timestamp in native code.
450    public boolean addNgramEntry(final PrevWordsInfo prevWordsInfo, final String word,
451            final int probability, final int timestamp) {
452        if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) {
453            return false;
454        }
455        final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][];
456        final boolean[] isBeginningOfSentenceArray =
457                new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
458        prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
459        final int[] wordCodePoints = StringUtils.toCodePointArray(word);
460        if (!addNgramEntryNative(mNativeDict, prevWordCodePointArrays,
461                isBeginningOfSentenceArray, wordCodePoints, probability, timestamp)) {
462            return false;
463        }
464        mHasUpdated = true;
465        return true;
466    }
467
468    // Remove an n-gram entry from the binary dictionary in native code.
469    public boolean removeNgramEntry(final PrevWordsInfo prevWordsInfo, final String word) {
470        if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) {
471            return false;
472        }
473        final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][];
474        final boolean[] isBeginningOfSentenceArray =
475                new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
476        prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
477        final int[] wordCodePoints = StringUtils.toCodePointArray(word);
478        if (!removeNgramEntryNative(mNativeDict, prevWordCodePointArrays,
479                isBeginningOfSentenceArray, wordCodePoints)) {
480            return false;
481        }
482        mHasUpdated = true;
483        return true;
484    }
485
486    public void addMultipleDictionaryEntries(final LanguageModelParam[] languageModelParams) {
487        if (!isValidDictionary()) return;
488        int processedParamCount = 0;
489        while (processedParamCount < languageModelParams.length) {
490            if (needsToRunGC(true /* mindsBlockByGC */)) {
491                flushWithGC();
492            }
493            processedParamCount = addMultipleDictionaryEntriesNative(mNativeDict,
494                    languageModelParams, processedParamCount);
495            mHasUpdated = true;
496            if (processedParamCount <= 0) {
497                return;
498            }
499        }
500    }
501
502    private void reopen() {
503        close();
504        final File dictFile = new File(mDictFilePath);
505        // WARNING: Because we pass 0 as the offset and file.length() as the length, this can
506        // only be called for actual files. Right now it's only called by the flush() family of
507        // functions, which require an updatable dictionary, so it's okay. But beware.
508        loadDictionary(dictFile.getAbsolutePath(), 0 /* startOffset */,
509                dictFile.length(), mIsUpdatable);
510    }
511
512    // Flush to dict file if the dictionary has been updated.
513    public boolean flush() {
514        if (!isValidDictionary()) return false;
515        if (mHasUpdated) {
516            if (!flushNative(mNativeDict, mDictFilePath)) {
517                return false;
518            }
519            reopen();
520        }
521        return true;
522    }
523
524    // Run GC and flush to dict file if the dictionary has been updated.
525    public boolean flushWithGCIfHasUpdated() {
526        if (mHasUpdated) {
527            return flushWithGC();
528        }
529        return true;
530    }
531
532    // Run GC and flush to dict file.
533    public boolean flushWithGC() {
534        if (!isValidDictionary()) return false;
535        if (!flushWithGCNative(mNativeDict, mDictFilePath)) {
536            return false;
537        }
538        reopen();
539        return true;
540    }
541
542    /**
543     * Checks whether GC is needed to run or not.
544     * @param mindsBlockByGC Whether to mind operations blocked by GC. We don't need to care about
545     * the blocking in some situations such as in idle time or just before closing.
546     * @return whether GC is needed to run or not.
547     */
548    public boolean needsToRunGC(final boolean mindsBlockByGC) {
549        if (!isValidDictionary()) return false;
550        return needsToRunGCNative(mNativeDict, mindsBlockByGC);
551    }
552
553    public boolean migrateTo(final int newFormatVersion) {
554        if (!isValidDictionary()) {
555            return false;
556        }
557        final String tmpDictFilePath = mDictFilePath + DICT_FILE_NAME_SUFFIX_FOR_MIGRATION;
558        if (!migrateNative(mNativeDict, tmpDictFilePath, newFormatVersion)) {
559            return false;
560        }
561        close();
562        final File dictFile = new File(mDictFilePath);
563        final File tmpDictFile = new File(tmpDictFilePath);
564        if (!FileUtils.deleteRecursively(dictFile)) {
565            return false;
566        }
567        if (!BinaryDictionaryUtils.renameDict(tmpDictFile, dictFile)) {
568            return false;
569        }
570        loadDictionary(dictFile.getAbsolutePath(), 0 /* startOffset */,
571                dictFile.length(), mIsUpdatable);
572        return true;
573    }
574
575    @UsedForTesting
576    public String getPropertyForTest(final String query) {
577        if (!isValidDictionary()) return "";
578        return getPropertyNative(mNativeDict, query);
579    }
580
581    @Override
582    public boolean shouldAutoCommit(final SuggestedWordInfo candidate) {
583        return candidate.mAutoCommitFirstWordConfidence > CONFIDENCE_TO_AUTO_COMMIT;
584    }
585
586    @Override
587    public void close() {
588        synchronized (mDicTraverseSessions) {
589            final int sessionsSize = mDicTraverseSessions.size();
590            for (int index = 0; index < sessionsSize; ++index) {
591                final DicTraverseSession traverseSession = mDicTraverseSessions.valueAt(index);
592                if (traverseSession != null) {
593                    traverseSession.close();
594                }
595            }
596            mDicTraverseSessions.clear();
597        }
598        closeInternalLocked();
599    }
600
601    private synchronized void closeInternalLocked() {
602        if (mNativeDict != 0) {
603            closeNative(mNativeDict);
604            mNativeDict = 0;
605        }
606    }
607
608    // TODO: Manage BinaryDictionary instances without using WeakReference or something.
609    @Override
610    protected void finalize() throws Throwable {
611        try {
612            closeInternalLocked();
613        } finally {
614            super.finalize();
615        }
616    }
617}
618