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