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