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