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