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