Home | History | Annotate | Download | only in latin
      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      */
    111     protected abstract void loadInitialContentsLocked();
    112 
    113     private boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) {
    114         return formatVersion == FormatSpec.VERSION4;
    115     }
    116 
    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 
    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      */
    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 
    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 
    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 
    162     private void asyncExecuteTaskWithWriteLock(final Runnable task) {
    163         asyncExecuteTaskWithLock(mLock.writeLock(), task);
    164     }
    165 
    166     private void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) {
    167         asyncPreCheckAndExecuteTaskWithLock(lock, null /* preCheckTask */, task);
    168     }
    169 
    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.
    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
    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 
    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 
    234     private void removeBinaryDictionary() {
    235         asyncExecuteTaskWithWriteLock(new Runnable() {
    236             @Override
    237             public void run() {
    238                 removeBinaryDictionaryLocked();
    239             }
    240         });
    241     }
    242 
    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 
    253     private void openBinaryDictionaryLocked() {
    254         mBinaryDictionary = new BinaryDictionary(
    255                 mDictFile.getAbsolutePath(), 0 /* offset */, mDictFile.length(),
    256                 true /* useFullEditDistance */, mLocale, mDictType, true /* isUpdatable */);
    257     }
    258 
    259     private void createOnMemoryBinaryDictionaryLocked() {
    260         mBinaryDictionary = new BinaryDictionary(
    261                 mDictFile.getAbsolutePath(), true /* useFullEditDistance */, mLocale, mDictType,
    262                 DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
    263     }
    264 
    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      */
    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 
    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      */
    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 
    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      */
    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      */
    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 
    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
    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 {
    407         public void onFinished();
    408     }
    409 
    410     /**
    411      * Dynamically add multiple entries to the dictionary.
    412      */
    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
    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
    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 
    493     protected boolean isInDictionaryLocked(final String word) {
    494         if (mBinaryDictionary == null) return false;
    495         return mBinaryDictionary.isInDictionary(word);
    496     }
    497 
    498     @Override
    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 
    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      */
    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      */
    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      */
    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      */
    581     public final void reloadDictionaryIfRequired() {
    582         if (!isReloadRequired()) return;
    583         asyncReloadDictionary();
    584     }
    585 
    586     /**
    587      * Returns whether a dictionary reload is required.
    588      */
    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      */
    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      */
    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
    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
    664     public void clearAndFlushDictionaryWithAdditionalAttributes(
    665             final Map<String, String> attributeMap) {
    666         mAdditionalAttributeMap = attributeMap;
    667         clear();
    668     }
    669 
    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