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.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      */
    115     protected abstract void loadInitialContentsLocked();
    116 
    117     static boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) {
    118         return formatVersion == FormatSpec.VERSION4;
    119     }
    120 
    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 
    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      */
    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 
    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 
    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 
    165     private void asyncExecuteTaskWithWriteLock(final Runnable task) {
    166         asyncExecuteTaskWithLock(mLock.writeLock(), task);
    167     }
    168 
    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
    184     BinaryDictionary getBinaryDictionary() {
    185         return mBinaryDictionary;
    186     }
    187 
    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
    199     public void close() {
    200         asyncExecuteTaskWithWriteLock(new Runnable() {
    201             @Override
    202             public void run() {
    203                 closeBinaryDictionary();
    204             }
    205         });
    206     }
    207 
    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 
    220     private void removeBinaryDictionary() {
    221         asyncExecuteTaskWithWriteLock(new Runnable() {
    222             @Override
    223             public void run() {
    224                 removeBinaryDictionaryLocked();
    225             }
    226         });
    227     }
    228 
    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 
    236     private void openBinaryDictionaryLocked() {
    237         mBinaryDictionary = new BinaryDictionary(
    238                 mDictFile.getAbsolutePath(), 0 /* offset */, mDictFile.length(),
    239                 true /* useFullEditDistance */, mLocale, mDictType, true /* isUpdatable */);
    240     }
    241 
    242     void createOnMemoryBinaryDictionaryLocked() {
    243         mBinaryDictionary = new BinaryDictionary(
    244                 mDictFile.getAbsolutePath(), true /* useFullEditDistance */, mLocale, mDictType,
    245                 DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
    246     }
    247 
    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      */
    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 
    273     protected void runGCIfRequiredLocked(final boolean mindsBlockByGC) {
    274         if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) {
    275             mBinaryDictionary.flushWithGC();
    276         }
    277     }
    278 
    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      */
    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 
    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      */
    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      */
    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 
    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      */
    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 {
    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
    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
    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
    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 
    486     protected boolean isInDictionaryLocked(final String word) {
    487         if (mBinaryDictionary == null) return false;
    488         return mBinaryDictionary.isInDictionary(word);
    489     }
    490 
    491     @Override
    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      */
    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      */
    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      */
    560     protected void setNeedsToRecreate() {
    561         mNeedsToRecreate = true;
    562     }
    563 
    564     void clearNeedsToRecreate() {
    565         mNeedsToRecreate = false;
    566     }
    567 
    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      */
    578     public final void reloadDictionaryIfRequired() {
    579         if (!isReloadRequired()) return;
    580         asyncReloadDictionary();
    581     }
    582 
    583     /**
    584      * Returns whether a dictionary reload is required.
    585      */
    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      */
    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      */
    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 
    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
    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
    681     public void clearAndFlushDictionaryWithAdditionalAttributes(
    682             final Map<String, String> attributeMap) {
    683         mAdditionalAttributeMap = attributeMap;
    684         clear();
    685     }
    686 
    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      */
    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