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