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