1 /*
2  * Copyright (C) 2012 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 
17 package com.android.inputmethod.latin;
18 
19 import android.content.Context;
20 import android.util.Log;
21 
22 import com.android.inputmethod.annotations.UsedForTesting;
23 import com.android.inputmethod.keyboard.ProximityInfo;
24 import com.android.inputmethod.latin.makedict.DictionaryHeader;
25 import com.android.inputmethod.latin.makedict.FormatSpec;
26 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
27 import com.android.inputmethod.latin.makedict.WordProperty;
28 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
29 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
30 import com.android.inputmethod.latin.utils.CombinedFormatUtils;
31 import com.android.inputmethod.latin.utils.DistracterFilter;
32 import com.android.inputmethod.latin.utils.ExecutorUtils;
33 import com.android.inputmethod.latin.utils.FileUtils;
34 import com.android.inputmethod.latin.utils.LanguageModelParam;
35 
36 import java.io.File;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.Locale;
40 import java.util.Map;
41 import java.util.concurrent.Callable;
42 import java.util.concurrent.CountDownLatch;
43 import java.util.concurrent.TimeUnit;
44 import java.util.concurrent.atomic.AtomicBoolean;
45 import java.util.concurrent.locks.Lock;
46 import java.util.concurrent.locks.ReentrantReadWriteLock;
47 
48 /**
49  * Abstract base class for an expandable dictionary that can be created and updated dynamically
50  * during runtime. When updated it automatically generates a new binary dictionary to handle future
51  * queries in native code. This binary dictionary is written to internal storage.
52  */
53 abstract public class ExpandableBinaryDictionary extends Dictionary {
54     private static final boolean DEBUG = false;
55 
56     /** Used for Log actions from this class */
57     private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName();
58 
59     /** Whether to print debug output to log */
60     private static final boolean DBG_STRESS_TEST = false;
61 
62     private static final int TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS = 100;
63 
64     private static final int DEFAULT_MAX_UNIGRAM_COUNT = 10000;
65     private static final int DEFAULT_MAX_BIGRAM_COUNT = 10000;
66 
67     /**
68      * The maximum length of a word in this dictionary.
69      */
70     protected static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
71 
72     private static final int DICTIONARY_FORMAT_VERSION = FormatSpec.VERSION4;
73 
74     /** The application context. */
75     protected final Context mContext;
76 
77     /**
78      * The binary dictionary generated dynamically from the fusion dictionary. This is used to
79      * answer unigram and bigram queries.
80      */
81     private BinaryDictionary mBinaryDictionary;
82 
83     /**
84      * The name of this dictionary, used as a part of the filename for storing the binary
85      * dictionary.
86      */
87     private final String mDictName;
88 
89     /** Dictionary locale */
90     private final Locale mLocale;
91 
92     /** Dictionary file */
93     private final File mDictFile;
94 
95     /** Indicates whether a task for reloading the dictionary has been scheduled. */
96     private final AtomicBoolean mIsReloading;
97 
98     /** Indicates whether the current dictionary needs to be recreated. */
99     private boolean mNeedsToRecreate;
100 
101     private final ReentrantReadWriteLock mLock;
102 
103     private Map<String, String> mAdditionalAttributeMap = null;
104 
105     /* A extension for a binary dictionary file. */
106     protected static final String DICT_FILE_EXTENSION = ".dict";
107 
108     /**
109      * Abstract method for loading initial contents of a given dictionary.
110      */
loadInitialContentsLocked()111     protected abstract void loadInitialContentsLocked();
112 
matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion)113     private boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) {
114         return formatVersion == FormatSpec.VERSION4;
115     }
116 
needsToMigrateDictionary(final int formatVersion)117     private boolean needsToMigrateDictionary(final int formatVersion) {
118         // When we bump up the dictionary format version, the old version should be added to here
119         // for supporting migration. Note that native code has to support reading such formats.
120         return formatVersion == FormatSpec.VERSION4_ONLY_FOR_TESTING;
121     }
122 
isValidDictionaryLocked()123     public boolean isValidDictionaryLocked() {
124         return mBinaryDictionary.isValidDictionary();
125     }
126 
127     /**
128      * Creates a new expandable binary dictionary.
129      *
130      * @param context The application context of the parent.
131      * @param dictName The name of the dictionary. Multiple instances with the same
132      *        name is supported.
133      * @param locale the dictionary locale.
134      * @param dictType the dictionary type, as a human-readable string
135      * @param dictFile dictionary file path. if null, use default dictionary path based on
136      *        dictionary type.
137      */
ExpandableBinaryDictionary(final Context context, final String dictName, final Locale locale, final String dictType, final File dictFile)138     public ExpandableBinaryDictionary(final Context context, final String dictName,
139             final Locale locale, final String dictType, final File dictFile) {
140         super(dictType);
141         mDictName = dictName;
142         mContext = context;
143         mLocale = locale;
144         mDictFile = getDictFile(context, dictName, dictFile);
145         mBinaryDictionary = null;
146         mIsReloading = new AtomicBoolean();
147         mNeedsToRecreate = false;
148         mLock = new ReentrantReadWriteLock();
149     }
150 
getDictFile(final Context context, final String dictName, final File dictFile)151     public static File getDictFile(final Context context, final String dictName,
152             final File dictFile) {
153         return (dictFile != null) ? dictFile
154                 : new File(context.getFilesDir(), dictName + DICT_FILE_EXTENSION);
155     }
156 
getDictName(final String name, final Locale locale, final File dictFile)157     public static String getDictName(final String name, final Locale locale,
158             final File dictFile) {
159         return dictFile != null ? dictFile.getName() : name + "." + locale.toString();
160     }
161 
asyncExecuteTaskWithWriteLock(final Runnable task)162     private void asyncExecuteTaskWithWriteLock(final Runnable task) {
163         asyncExecuteTaskWithLock(mLock.writeLock(), task);
164     }
165 
asyncExecuteTaskWithLock(final Lock lock, final Runnable task)166     private void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) {
167         asyncPreCheckAndExecuteTaskWithLock(lock, null /* preCheckTask */, task);
168     }
169 
asyncPreCheckAndExecuteTaskWithWriteLock( final Callable<Boolean> preCheckTask, final Runnable task)170     private void asyncPreCheckAndExecuteTaskWithWriteLock(
171             final Callable<Boolean> preCheckTask, final Runnable task) {
172         asyncPreCheckAndExecuteTaskWithLock(mLock.writeLock(), preCheckTask, task);
173 
174     }
175 
176     // Execute task with lock when the result of preCheckTask is true or preCheckTask is null.
asyncPreCheckAndExecuteTaskWithLock(final Lock lock, final Callable<Boolean> preCheckTask, final Runnable task)177     private void asyncPreCheckAndExecuteTaskWithLock(final Lock lock,
178             final Callable<Boolean> preCheckTask, final Runnable task) {
179         ExecutorUtils.getExecutor(mDictName).execute(new Runnable() {
180             @Override
181             public void run() {
182                 if (preCheckTask != null) {
183                     try {
184                         if (!preCheckTask.call().booleanValue()) {
185                             return;
186                         }
187                     } catch (final Exception e) {
188                         Log.e(TAG, "The pre check task throws an exception.", e);
189                         return;
190                     }
191                 }
192                 lock.lock();
193                 try {
194                     task.run();
195                 } finally {
196                     lock.unlock();
197                 }
198             }
199         });
200     }
201 
202     /**
203      * Closes and cleans up the binary dictionary.
204      */
205     @Override
close()206     public void close() {
207         asyncExecuteTaskWithWriteLock(new Runnable() {
208             @Override
209             public void run() {
210                 if (mBinaryDictionary != null) {
211                     mBinaryDictionary.close();
212                     mBinaryDictionary = null;
213                 }
214             }
215         });
216     }
217 
getHeaderAttributeMap()218     protected Map<String, String> getHeaderAttributeMap() {
219         HashMap<String, String> attributeMap = new HashMap<>();
220         if (mAdditionalAttributeMap != null) {
221             attributeMap.putAll(mAdditionalAttributeMap);
222         }
223         attributeMap.put(DictionaryHeader.DICTIONARY_ID_KEY, mDictName);
224         attributeMap.put(DictionaryHeader.DICTIONARY_LOCALE_KEY, mLocale.toString());
225         attributeMap.put(DictionaryHeader.DICTIONARY_VERSION_KEY,
226                 String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
227         attributeMap.put(DictionaryHeader.MAX_UNIGRAM_COUNT_KEY,
228                 String.valueOf(DEFAULT_MAX_UNIGRAM_COUNT));
229         attributeMap.put(DictionaryHeader.MAX_BIGRAM_COUNT_KEY,
230                 String.valueOf(DEFAULT_MAX_BIGRAM_COUNT));
231         return attributeMap;
232     }
233 
removeBinaryDictionary()234     private void removeBinaryDictionary() {
235         asyncExecuteTaskWithWriteLock(new Runnable() {
236             @Override
237             public void run() {
238                 removeBinaryDictionaryLocked();
239             }
240         });
241     }
242 
removeBinaryDictionaryLocked()243     private void removeBinaryDictionaryLocked() {
244         if (mBinaryDictionary != null) {
245             mBinaryDictionary.close();
246         }
247         if (mDictFile.exists() && !FileUtils.deleteRecursively(mDictFile)) {
248             Log.e(TAG, "Can't remove a file: " + mDictFile.getName());
249         }
250         mBinaryDictionary = null;
251     }
252 
openBinaryDictionaryLocked()253     private void openBinaryDictionaryLocked() {
254         mBinaryDictionary = new BinaryDictionary(
255                 mDictFile.getAbsolutePath(), 0 /* offset */, mDictFile.length(),
256                 true /* useFullEditDistance */, mLocale, mDictType, true /* isUpdatable */);
257     }
258 
createOnMemoryBinaryDictionaryLocked()259     private void createOnMemoryBinaryDictionaryLocked() {
260         mBinaryDictionary = new BinaryDictionary(
261                 mDictFile.getAbsolutePath(), true /* useFullEditDistance */, mLocale, mDictType,
262                 DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
263     }
264 
clear()265     public void clear() {
266         asyncExecuteTaskWithWriteLock(new Runnable() {
267             @Override
268             public void run() {
269                 removeBinaryDictionaryLocked();
270                 createOnMemoryBinaryDictionaryLocked();
271             }
272         });
273     }
274 
275     /**
276      * Check whether GC is needed and run GC if required.
277      */
runGCIfRequired(final boolean mindsBlockByGC)278     protected void runGCIfRequired(final boolean mindsBlockByGC) {
279         asyncExecuteTaskWithWriteLock(new Runnable() {
280             @Override
281             public void run() {
282                 if (mBinaryDictionary == null) {
283                     return;
284                 }
285                 runGCIfRequiredLocked(mindsBlockByGC);
286             }
287         });
288     }
289 
runGCIfRequiredLocked(final boolean mindsBlockByGC)290     protected void runGCIfRequiredLocked(final boolean mindsBlockByGC) {
291         if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) {
292             mBinaryDictionary.flushWithGC();
293         }
294     }
295 
296     /**
297      * Adds unigram information of a word to the dictionary. May overwrite an existing entry.
298      */
addUnigramEntryWithCheckingDistracter(final String word, final int frequency, final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, final boolean isBlacklisted, final int timestamp, final DistracterFilter distracterFilter)299     public void addUnigramEntryWithCheckingDistracter(final String word, final int frequency,
300             final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord,
301             final boolean isBlacklisted, final int timestamp,
302             final DistracterFilter distracterFilter) {
303         reloadDictionaryIfRequired();
304         asyncPreCheckAndExecuteTaskWithWriteLock(
305                 new Callable<Boolean>() {
306                     @Override
307                     public Boolean call() throws Exception {
308                         return !distracterFilter.isDistracterToWordsInDictionaries(
309                                 PrevWordsInfo.EMPTY_PREV_WORDS_INFO, word, mLocale);
310                     }
311                 },
312                 new Runnable() {
313                     @Override
314                     public void run() {
315                         if (mBinaryDictionary == null) {
316                             return;
317                         }
318                         runGCIfRequiredLocked(true /* mindsBlockByGC */);
319                         addUnigramLocked(word, frequency, shortcutTarget, shortcutFreq,
320                                 isNotAWord, isBlacklisted, timestamp);
321                     }
322                 });
323     }
324 
addUnigramLocked(final String word, final int frequency, final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, final boolean isBlacklisted, final int timestamp)325     protected void addUnigramLocked(final String word, final int frequency,
326             final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord,
327             final boolean isBlacklisted, final int timestamp) {
328         if (!mBinaryDictionary.addUnigramEntry(word, frequency, shortcutTarget, shortcutFreq,
329                 false /* isBeginningOfSentence */, isNotAWord, isBlacklisted, timestamp)) {
330             Log.e(TAG, "Cannot add unigram entry. word: " + word);
331         }
332     }
333 
334     /**
335      * Dynamically remove the unigram entry from the dictionary.
336      */
removeUnigramEntryDynamically(final String word)337     public void removeUnigramEntryDynamically(final String word) {
338         reloadDictionaryIfRequired();
339         asyncExecuteTaskWithWriteLock(new Runnable() {
340             @Override
341             public void run() {
342                 if (mBinaryDictionary == null) {
343                     return;
344                 }
345                 runGCIfRequiredLocked(true /* mindsBlockByGC */);
346                 if (!mBinaryDictionary.removeUnigramEntry(word)) {
347                     if (DEBUG) {
348                         Log.i(TAG, "Cannot remove unigram entry: " + word);
349                     }
350                 }
351             }
352         });
353     }
354 
355     /**
356      * Adds n-gram information of a word to the dictionary. May overwrite an existing entry.
357      */
addNgramEntry(final PrevWordsInfo prevWordsInfo, final String word, final int frequency, final int timestamp)358     public void addNgramEntry(final PrevWordsInfo prevWordsInfo, final String word,
359             final int frequency, final int timestamp) {
360         reloadDictionaryIfRequired();
361         asyncExecuteTaskWithWriteLock(new Runnable() {
362             @Override
363             public void run() {
364                 if (mBinaryDictionary == null) {
365                     return;
366                 }
367                 runGCIfRequiredLocked(true /* mindsBlockByGC */);
368                 addNgramEntryLocked(prevWordsInfo, word, frequency, timestamp);
369             }
370         });
371     }
372 
addNgramEntryLocked(final PrevWordsInfo prevWordsInfo, final String word, final int frequency, final int timestamp)373     protected void addNgramEntryLocked(final PrevWordsInfo prevWordsInfo, final String word,
374             final int frequency, final int timestamp) {
375         if (!mBinaryDictionary.addNgramEntry(prevWordsInfo, word, frequency, timestamp)) {
376             if (DEBUG) {
377                 Log.i(TAG, "Cannot add n-gram entry.");
378                 Log.i(TAG, "  PrevWordsInfo: " + prevWordsInfo + ", word: " + word);
379             }
380         }
381     }
382 
383     /**
384      * Dynamically remove the n-gram entry in the dictionary.
385      */
386     @UsedForTesting
removeNgramDynamically(final PrevWordsInfo prevWordsInfo, final String word)387     public void removeNgramDynamically(final PrevWordsInfo prevWordsInfo, final String word) {
388         reloadDictionaryIfRequired();
389         asyncExecuteTaskWithWriteLock(new Runnable() {
390             @Override
391             public void run() {
392                 if (mBinaryDictionary == null) {
393                     return;
394                 }
395                 runGCIfRequiredLocked(true /* mindsBlockByGC */);
396                 if (!mBinaryDictionary.removeNgramEntry(prevWordsInfo, word)) {
397                     if (DEBUG) {
398                         Log.i(TAG, "Cannot remove n-gram entry.");
399                         Log.i(TAG, "  PrevWordsInfo: " + prevWordsInfo + ", word: " + word);
400                     }
401                 }
402             }
403         });
404     }
405 
406     public interface AddMultipleDictionaryEntriesCallback {
onFinished()407         public void onFinished();
408     }
409 
410     /**
411      * Dynamically add multiple entries to the dictionary.
412      */
addMultipleDictionaryEntriesDynamically( final ArrayList<LanguageModelParam> languageModelParams, final AddMultipleDictionaryEntriesCallback callback)413     public void addMultipleDictionaryEntriesDynamically(
414             final ArrayList<LanguageModelParam> languageModelParams,
415             final AddMultipleDictionaryEntriesCallback callback) {
416         reloadDictionaryIfRequired();
417         asyncExecuteTaskWithWriteLock(new Runnable() {
418             @Override
419             public void run() {
420                 try {
421                     if (mBinaryDictionary == null) {
422                         return;
423                     }
424                     mBinaryDictionary.addMultipleDictionaryEntries(
425                             languageModelParams.toArray(
426                                     new LanguageModelParam[languageModelParams.size()]));
427                 } finally {
428                     if (callback != null) {
429                         callback.onFinished();
430                     }
431                 }
432             }
433         });
434     }
435 
436     @Override
getSuggestions(final WordComposer composer, final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId, final float[] inOutLanguageWeight)437     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
438             final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
439             final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId,
440             final float[] inOutLanguageWeight) {
441         reloadDictionaryIfRequired();
442         boolean lockAcquired = false;
443         try {
444             lockAcquired = mLock.readLock().tryLock(
445                     TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
446             if (lockAcquired) {
447                 if (mBinaryDictionary == null) {
448                     return null;
449                 }
450                 final ArrayList<SuggestedWordInfo> suggestions =
451                         mBinaryDictionary.getSuggestions(composer, prevWordsInfo, proximityInfo,
452                                 settingsValuesForSuggestion, sessionId, inOutLanguageWeight);
453                 if (mBinaryDictionary.isCorrupted()) {
454                     Log.i(TAG, "Dictionary (" + mDictName +") is corrupted. "
455                             + "Remove and regenerate it.");
456                     removeBinaryDictionary();
457                 }
458                 return suggestions;
459             }
460         } catch (final InterruptedException e) {
461             Log.e(TAG, "Interrupted tryLock() in getSuggestionsWithSessionId().", e);
462         } finally {
463             if (lockAcquired) {
464                 mLock.readLock().unlock();
465             }
466         }
467         return null;
468     }
469 
470     @Override
isInDictionary(final String word)471     public boolean isInDictionary(final String word) {
472         reloadDictionaryIfRequired();
473         boolean lockAcquired = false;
474         try {
475             lockAcquired = mLock.readLock().tryLock(
476                     TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
477             if (lockAcquired) {
478                 if (mBinaryDictionary == null) {
479                     return false;
480                 }
481                 return isInDictionaryLocked(word);
482             }
483         } catch (final InterruptedException e) {
484             Log.e(TAG, "Interrupted tryLock() in isInDictionary().", e);
485         } finally {
486             if (lockAcquired) {
487                 mLock.readLock().unlock();
488             }
489         }
490         return false;
491     }
492 
isInDictionaryLocked(final String word)493     protected boolean isInDictionaryLocked(final String word) {
494         if (mBinaryDictionary == null) return false;
495         return mBinaryDictionary.isInDictionary(word);
496     }
497 
498     @Override
getMaxFrequencyOfExactMatches(final String word)499     public int getMaxFrequencyOfExactMatches(final String word) {
500         reloadDictionaryIfRequired();
501         boolean lockAcquired = false;
502         try {
503             lockAcquired = mLock.readLock().tryLock(
504                     TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
505             if (lockAcquired) {
506                 if (mBinaryDictionary == null) {
507                     return NOT_A_PROBABILITY;
508                 }
509                 return mBinaryDictionary.getMaxFrequencyOfExactMatches(word);
510             }
511         } catch (final InterruptedException e) {
512             Log.e(TAG, "Interrupted tryLock() in getMaxFrequencyOfExactMatches().", e);
513         } finally {
514             if (lockAcquired) {
515                 mLock.readLock().unlock();
516             }
517         }
518         return NOT_A_PROBABILITY;
519     }
520 
521 
isValidNgramLocked(final PrevWordsInfo prevWordsInfo, final String word)522     protected boolean isValidNgramLocked(final PrevWordsInfo prevWordsInfo, final String word) {
523         if (mBinaryDictionary == null) return false;
524         return mBinaryDictionary.isValidNgram(prevWordsInfo, word);
525     }
526 
527     /**
528      * Loads the current binary dictionary from internal storage. Assumes the dictionary file
529      * exists.
530      */
loadBinaryDictionaryLocked()531     private void loadBinaryDictionaryLocked() {
532         if (DBG_STRESS_TEST) {
533             // Test if this class does not cause problems when it takes long time to load binary
534             // dictionary.
535             try {
536                 Log.w(TAG, "Start stress in loading: " + mDictName);
537                 Thread.sleep(15000);
538                 Log.w(TAG, "End stress in loading");
539             } catch (InterruptedException e) {
540             }
541         }
542         final BinaryDictionary oldBinaryDictionary = mBinaryDictionary;
543         openBinaryDictionaryLocked();
544         if (oldBinaryDictionary != null) {
545             oldBinaryDictionary.close();
546         }
547         if (mBinaryDictionary.isValidDictionary()
548                 && needsToMigrateDictionary(mBinaryDictionary.getFormatVersion())) {
549             if (!mBinaryDictionary.migrateTo(DICTIONARY_FORMAT_VERSION)) {
550                 Log.e(TAG, "Dictionary migration failed: " + mDictName);
551                 removeBinaryDictionaryLocked();
552             }
553         }
554     }
555 
556     /**
557      * Create a new binary dictionary and load initial contents.
558      */
createNewDictionaryLocked()559     private void createNewDictionaryLocked() {
560         removeBinaryDictionaryLocked();
561         createOnMemoryBinaryDictionaryLocked();
562         loadInitialContentsLocked();
563         // Run GC and flush to file when initial contents have been loaded.
564         mBinaryDictionary.flushWithGCIfHasUpdated();
565     }
566 
567     /**
568      * Marks that the dictionary needs to be recreated.
569      *
570      */
setNeedsToRecreate()571     protected void setNeedsToRecreate() {
572         mNeedsToRecreate = true;
573     }
574 
575     /**
576      * Load the current binary dictionary from internal storage. If the dictionary file doesn't
577      * exists or needs to be regenerated, the new dictionary file will be asynchronously generated.
578      * However, the dictionary itself is accessible even before the new dictionary file is actually
579      * generated. It may return a null result for getSuggestions() in that case by design.
580      */
reloadDictionaryIfRequired()581     public final void reloadDictionaryIfRequired() {
582         if (!isReloadRequired()) return;
583         asyncReloadDictionary();
584     }
585 
586     /**
587      * Returns whether a dictionary reload is required.
588      */
isReloadRequired()589     private boolean isReloadRequired() {
590         return mBinaryDictionary == null || mNeedsToRecreate;
591     }
592 
593     /**
594      * Reloads the dictionary. Access is controlled on a per dictionary file basis.
595      */
asyncReloadDictionary()596     private final void asyncReloadDictionary() {
597         if (mIsReloading.compareAndSet(false, true)) {
598             asyncExecuteTaskWithWriteLock(new Runnable() {
599                 @Override
600                 public void run() {
601                     try {
602                         if (!mDictFile.exists() || mNeedsToRecreate) {
603                             // If the dictionary file does not exist or contents have been updated,
604                             // generate a new one.
605                             createNewDictionaryLocked();
606                         } else if (mBinaryDictionary == null) {
607                             // Otherwise, load the existing dictionary.
608                             loadBinaryDictionaryLocked();
609                             if (mBinaryDictionary != null && !(isValidDictionaryLocked()
610                                     // TODO: remove the check below
611                                     && matchesExpectedBinaryDictFormatVersionForThisType(
612                                             mBinaryDictionary.getFormatVersion()))) {
613                                 // Binary dictionary or its format version is not valid. Regenerate
614                                 // the dictionary file. createNewDictionaryLocked will remove the
615                                 // existing files if appropriate.
616                                 createNewDictionaryLocked();
617                             }
618                         }
619                         mNeedsToRecreate = false;
620                     } finally {
621                         mIsReloading.set(false);
622                     }
623                 }
624             });
625         }
626     }
627 
628     /**
629      * Flush binary dictionary to dictionary file.
630      */
asyncFlushBinaryDictionary()631     public void asyncFlushBinaryDictionary() {
632         asyncExecuteTaskWithWriteLock(new Runnable() {
633             @Override
634             public void run() {
635                 if (mBinaryDictionary == null) {
636                     return;
637                 }
638                 if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
639                     mBinaryDictionary.flushWithGC();
640                 } else {
641                     mBinaryDictionary.flush();
642                 }
643             }
644         });
645     }
646 
647     @UsedForTesting
waitAllTasksForTests()648     public void waitAllTasksForTests() {
649         final CountDownLatch countDownLatch = new CountDownLatch(1);
650         ExecutorUtils.getExecutor(mDictName).execute(new Runnable() {
651             @Override
652             public void run() {
653                 countDownLatch.countDown();
654             }
655         });
656         try {
657             countDownLatch.await();
658         } catch (InterruptedException e) {
659             Log.e(TAG, "Interrupted while waiting for finishing dictionary operations.", e);
660         }
661     }
662 
663     @UsedForTesting
clearAndFlushDictionaryWithAdditionalAttributes( final Map<String, String> attributeMap)664     public void clearAndFlushDictionaryWithAdditionalAttributes(
665             final Map<String, String> attributeMap) {
666         mAdditionalAttributeMap = attributeMap;
667         clear();
668     }
669 
dumpAllWordsForDebug()670     public void dumpAllWordsForDebug() {
671         reloadDictionaryIfRequired();
672         asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() {
673             @Override
674             public void run() {
675                 Log.d(TAG, "Dump dictionary: " + mDictName);
676                 try {
677                     final DictionaryHeader header = mBinaryDictionary.getHeader();
678                     Log.d(TAG, "Format version: " + mBinaryDictionary.getFormatVersion());
679                     Log.d(TAG, CombinedFormatUtils.formatAttributeMap(
680                             header.mDictionaryOptions.mAttributes));
681                 } catch (final UnsupportedFormatException e) {
682                     Log.d(TAG, "Cannot fetch header information.", e);
683                 }
684                 int token = 0;
685                 do {
686                     final BinaryDictionary.GetNextWordPropertyResult result =
687                             mBinaryDictionary.getNextWordProperty(token);
688                     final WordProperty wordProperty = result.mWordProperty;
689                     if (wordProperty == null) {
690                         Log.d(TAG, " dictionary is empty.");
691                         break;
692                     }
693                     Log.d(TAG, wordProperty.toString());
694                     token = result.mNextToken;
695                 } while (token != 0);
696             }
697         });
698     }
699 }
700