ExpandableBinaryDictionary.java revision 5ed30a7660048ef4bf78077e77554c97786eae2b
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
17package com.android.inputmethod.latin;
18
19import android.content.Context;
20import android.os.SystemClock;
21import android.util.Log;
22
23import com.android.inputmethod.annotations.UsedForTesting;
24import com.android.inputmethod.keyboard.ProximityInfo;
25import com.android.inputmethod.latin.makedict.FormatSpec;
26import com.android.inputmethod.latin.personalization.DynamicPersonalizationDictionaryWriter;
27import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
28import com.android.inputmethod.latin.utils.AsyncResultHolder;
29import com.android.inputmethod.latin.utils.CollectionUtils;
30import com.android.inputmethod.latin.utils.PrioritizedSerialExecutor;
31
32import java.io.File;
33import java.util.ArrayList;
34import java.util.HashMap;
35import java.util.Map;
36import java.util.concurrent.ConcurrentHashMap;
37import java.util.concurrent.atomic.AtomicReference;
38
39/**
40 * Abstract base class for an expandable dictionary that can be created and updated dynamically
41 * during runtime. When updated it automatically generates a new binary dictionary to handle future
42 * queries in native code. This binary dictionary is written to internal storage, and potentially
43 * shared across multiple ExpandableBinaryDictionary instances. Updates to each dictionary filename
44 * are controlled across multiple instances to ensure that only one instance can update the same
45 * dictionary at the same time.
46 */
47abstract public class ExpandableBinaryDictionary extends Dictionary {
48
49    /** Used for Log actions from this class */
50    private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName();
51
52    /** Whether to print debug output to log */
53    private static boolean DEBUG = false;
54
55    // TODO: Remove.
56    /** Whether to call binary dictionary dynamically updating methods. */
57    public static boolean ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE = true;
58
59    private static final int TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS = 100;
60
61    /**
62     * The maximum length of a word in this dictionary.
63     */
64    protected static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
65
66    private static final int DICTIONARY_FORMAT_VERSION = 3;
67
68    private static final String SUPPORTS_DYNAMIC_UPDATE =
69            FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE;
70
71    /**
72     * A static map of time recorders, each of which records the time of accesses to a single binary
73     * dictionary file. The key for this map is the filename and the value is the shared dictionary
74     * time recorder associated with that filename.
75     */
76    private static volatile ConcurrentHashMap<String, DictionaryTimeRecorder>
77            sFilenameDictionaryTimeRecorderMap = CollectionUtils.newConcurrentHashMap();
78
79    private static volatile ConcurrentHashMap<String, PrioritizedSerialExecutor>
80            sFilenameExecutorMap = CollectionUtils.newConcurrentHashMap();
81
82    /** The application context. */
83    protected final Context mContext;
84
85    /**
86     * The binary dictionary generated dynamically from the fusion dictionary. This is used to
87     * answer unigram and bigram queries.
88     */
89    private BinaryDictionary mBinaryDictionary;
90
91    // TODO: Remove and handle dictionaries in native code.
92    /** The in-memory dictionary used to generate the binary dictionary. */
93    protected AbstractDictionaryWriter mDictionaryWriter;
94
95    /**
96     * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple
97     * dictionary instances with the same filename is supported, with access controlled by
98     * DictionaryTimeRecorder.
99     */
100    private final String mFilename;
101
102    /** Whether to support dynamically updating the dictionary */
103    private final boolean mIsUpdatable;
104
105    // TODO: remove, once dynamic operations is serialized
106    /** Records access to the shared binary dictionary file across multiple instances. */
107    private final DictionaryTimeRecorder mFilenameDictionaryTimeRecorder;
108
109    // TODO: remove, once dynamic operations is serialized
110    /** Records access to the local binary dictionary for this instance. */
111    private final DictionaryTimeRecorder mPerInstanceDictionaryTimeRecorder =
112            new DictionaryTimeRecorder();
113
114    /* A extension for a binary dictionary file. */
115    public static final String DICT_FILE_EXTENSION = ".dict";
116
117    private final AtomicReference<Runnable> mUnfinishedFlushingTask =
118            new AtomicReference<Runnable>();
119
120    /**
121     * Abstract method for loading the unigrams and bigrams of a given dictionary in a background
122     * thread.
123     */
124    protected abstract void loadDictionaryAsync();
125
126    /**
127     * Indicates that the source dictionary content has changed and a rebuild of the binary file is
128     * required. If it returns false, the next reload will only read the current binary dictionary
129     * from file. Note that the shared binary dictionary is locked when this is called.
130     */
131    protected abstract boolean hasContentChanged();
132
133    /**
134     * Gets the dictionary time recorder for the given filename.
135     */
136    private static DictionaryTimeRecorder getDictionaryTimeRecorder(
137            String filename) {
138        DictionaryTimeRecorder recorder = sFilenameDictionaryTimeRecorderMap.get(filename);
139        if (recorder == null) {
140            synchronized(sFilenameDictionaryTimeRecorderMap) {
141                recorder = new DictionaryTimeRecorder();
142                sFilenameDictionaryTimeRecorderMap.put(filename, recorder);
143            }
144        }
145        return recorder;
146    }
147
148    /**
149     * Gets the executor for the given filename.
150     */
151    private static PrioritizedSerialExecutor getExecutor(final String filename) {
152        PrioritizedSerialExecutor executor = sFilenameExecutorMap.get(filename);
153        if (executor == null) {
154            synchronized(sFilenameExecutorMap) {
155                executor = new PrioritizedSerialExecutor();
156                sFilenameExecutorMap.put(filename, executor);
157            }
158        }
159        return executor;
160    }
161
162    private static AbstractDictionaryWriter getDictionaryWriter(final Context context,
163            final String dictType, final boolean isDynamicPersonalizationDictionary) {
164        if (isDynamicPersonalizationDictionary) {
165            if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
166                return null;
167            } else {
168                return new DynamicPersonalizationDictionaryWriter(context, dictType);
169            }
170        } else {
171            return new DictionaryWriter(context, dictType);
172        }
173    }
174
175    /**
176     * Creates a new expandable binary dictionary.
177     *
178     * @param context The application context of the parent.
179     * @param filename The filename for this binary dictionary. Multiple dictionaries with the same
180     *        filename is supported.
181     * @param dictType the dictionary type, as a human-readable string
182     * @param isUpdatable whether to support dynamically updating the dictionary. Please note that
183     *        dynamic dictionary has negative effects on memory space and computation time.
184     */
185    public ExpandableBinaryDictionary(final Context context, final String filename,
186            final String dictType, final boolean isUpdatable) {
187        super(dictType);
188        mFilename = filename;
189        mContext = context;
190        mIsUpdatable = isUpdatable;
191        mBinaryDictionary = null;
192        mFilenameDictionaryTimeRecorder = getDictionaryTimeRecorder(filename);
193        // Currently, only dynamic personalization dictionary is updatable.
194        mDictionaryWriter = getDictionaryWriter(context, dictType, isUpdatable);
195    }
196
197    protected static String getFilenameWithLocale(final String name, final String localeStr) {
198        return name + "." + localeStr + DICT_FILE_EXTENSION;
199    }
200
201    /**
202     * Closes and cleans up the binary dictionary.
203     */
204    @Override
205    public void close() {
206        getExecutor(mFilename).execute(new Runnable() {
207            @Override
208            public void run() {
209                if (mBinaryDictionary!= null) {
210                    mBinaryDictionary.close();
211                    mBinaryDictionary = null;
212                }
213                if (mDictionaryWriter != null) {
214                    mDictionaryWriter.close();
215                }
216            }
217        });
218    }
219
220    protected void closeBinaryDictionary() {
221        // Ensure that no other threads are accessing the local binary dictionary.
222        getExecutor(mFilename).execute(new Runnable() {
223            @Override
224            public void run() {
225                if (mBinaryDictionary != null) {
226                    mBinaryDictionary.close();
227                    mBinaryDictionary = null;
228                }
229            }
230        });
231    }
232
233    protected Map<String, String> getHeaderAttributeMap() {
234        HashMap<String, String> attributeMap = new HashMap<String, String>();
235        attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE,
236                SUPPORTS_DYNAMIC_UPDATE);
237        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFilename);
238        return attributeMap;
239    }
240
241    protected void clear() {
242        getExecutor(mFilename).execute(new Runnable() {
243            @Override
244            public void run() {
245                if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE && mDictionaryWriter == null) {
246                    mBinaryDictionary.close();
247                    final File file = new File(mContext.getFilesDir(), mFilename);
248                    BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
249                            DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
250                } else {
251                    mDictionaryWriter.clear();
252                }
253            }
254        });
255    }
256
257    /**
258     * Adds a word unigram to the dictionary. Used for loading a dictionary.
259     */
260    protected void addWord(final String word, final String shortcutTarget,
261            final int frequency, final boolean isNotAWord) {
262        mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
263    }
264
265    /**
266     * Adds a word bigram in the dictionary. Used for loading a dictionary.
267     */
268    protected void addBigram(final String prevWord, final String word, final int frequency,
269            final long lastModifiedTime) {
270        mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */,
271                lastModifiedTime);
272    }
273
274    /**
275     * Dynamically adds a word unigram to the dictionary. May overwrite an existing entry.
276     */
277    protected void addWordDynamically(final String word, final String shortcutTarget,
278            final int frequency, final boolean isNotAWord) {
279        if (!mIsUpdatable) {
280            Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename);
281            return;
282        }
283
284        getExecutor(mFilename).execute(new Runnable() {
285            @Override
286            public void run() {
287                if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
288                    mBinaryDictionary.addUnigramWord(word, frequency);
289                } else {
290                    // TODO: Remove.
291                    mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
292                }
293            }
294        });
295    }
296
297    /**
298     * Dynamically adds a word bigram in the dictionary. May overwrite an existing entry.
299     */
300    protected void addBigramDynamically(final String word0, final String word1,
301            final int frequency, final boolean isValid) {
302        if (!mIsUpdatable) {
303            Log.w(TAG, "addBigramDynamically is called for non-updatable dictionary: "
304                    + mFilename);
305            return;
306        }
307
308        getExecutor(mFilename).execute(new Runnable() {
309            @Override
310            public void run() {
311                if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
312                    mBinaryDictionary.addBigramWords(word0, word1, frequency);
313                } else {
314                    // TODO: Remove.
315                    mDictionaryWriter.addBigramWords(word0, word1, frequency, isValid,
316                            0 /* lastTouchedTime */);
317                }
318            }
319        });
320    }
321
322    /**
323     * Dynamically remove a word bigram in the dictionary.
324     */
325    protected void removeBigramDynamically(final String word0, final String word1) {
326        if (!mIsUpdatable) {
327            Log.w(TAG, "removeBigramDynamically is called for non-updatable dictionary: "
328                    + mFilename);
329            return;
330        }
331
332        getExecutor(mFilename).execute(new Runnable() {
333            @Override
334            public void run() {
335                if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
336                    mBinaryDictionary.removeBigramWords(word0, word1);
337                } else {
338                    // TODO: Remove.
339                    mDictionaryWriter.removeBigramWords(word0, word1);
340                }
341            }
342        });
343    }
344
345    @Override
346    public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
347            final String prevWord, final ProximityInfo proximityInfo,
348            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
349            final int sessionId) {
350        reloadDictionaryIfRequired();
351        final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
352        final AsyncResultHolder<ArrayList<SuggestedWordInfo>> holder =
353                new AsyncResultHolder<ArrayList<SuggestedWordInfo>>();
354        getExecutor(mFilename).executePrioritized(new Runnable() {
355            @Override
356            public void run() {
357                if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
358                    if (mBinaryDictionary == null) {
359                        holder.set(null);
360                        return;
361                    }
362                    final ArrayList<SuggestedWordInfo> binarySuggestion =
363                            mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord,
364                                    proximityInfo, blockOffensiveWords, additionalFeaturesOptions,
365                                    sessionId);
366                    holder.set(binarySuggestion);
367                } else {
368                    final ArrayList<SuggestedWordInfo> inMemDictSuggestion =
369                            composer.isBatchMode() ? null :
370                                    mDictionaryWriter.getSuggestionsWithSessionId(composer,
371                                            prevWord, proximityInfo, blockOffensiveWords,
372                                            additionalFeaturesOptions, sessionId);
373                    // TODO: Remove checking mIsUpdatable and use native suggestion.
374                    if (mBinaryDictionary != null && !mIsUpdatable) {
375                        final ArrayList<SuggestedWordInfo> binarySuggestion =
376                                mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord,
377                                        proximityInfo, blockOffensiveWords,
378                                        additionalFeaturesOptions, sessionId);
379                        if (inMemDictSuggestion == null) {
380                            holder.set(binarySuggestion);
381                        } else if (binarySuggestion == null) {
382                            holder.set(inMemDictSuggestion);
383                        } else {
384                            binarySuggestion.addAll(inMemDictSuggestion);
385                            holder.set(binarySuggestion);
386                        }
387                    } else {
388                        holder.set(inMemDictSuggestion);
389                    }
390                }
391            }
392        });
393        return holder.get(null, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
394    }
395
396    @Override
397    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
398            final String prevWord, final ProximityInfo proximityInfo,
399            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
400        return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords,
401                additionalFeaturesOptions, 0 /* sessionId */);
402    }
403
404    @Override
405    public boolean isValidWord(final String word) {
406        reloadDictionaryIfRequired();
407        return isValidWordInner(word);
408    }
409
410    protected boolean isValidWordInner(final String word) {
411        final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>();
412        getExecutor(mFilename).executePrioritized(new Runnable() {
413            @Override
414            public void run() {
415                holder.set(isValidWordLocked(word));
416            }
417        });
418        return holder.get(false, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
419    }
420
421    protected boolean isValidWordLocked(final String word) {
422        if (mBinaryDictionary == null) return false;
423        return mBinaryDictionary.isValidWord(word);
424    }
425
426    protected boolean isValidBigramLocked(final String word1, final String word2) {
427        if (mBinaryDictionary == null) return false;
428        return mBinaryDictionary.isValidBigram(word1, word2);
429    }
430
431    /**
432     * Load the current binary dictionary from internal storage in a background thread. If no binary
433     * dictionary exists, this method will generate one.
434     */
435    protected void loadDictionary() {
436        mPerInstanceDictionaryTimeRecorder.mLastUpdateRequestTime = SystemClock.uptimeMillis();
437        reloadDictionaryIfRequired();
438    }
439
440    /**
441     * Loads the current binary dictionary from internal storage. Assumes the dictionary file
442     * exists.
443     */
444    private void loadBinaryDictionary() {
445        if (DEBUG) {
446            Log.d(TAG, "Loading binary dictionary: " + mFilename + " request="
447                    + mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime + " update="
448                    + mFilenameDictionaryTimeRecorder.mLastUpdateTime);
449        }
450
451        final File file = new File(mContext.getFilesDir(), mFilename);
452        final String filename = file.getAbsolutePath();
453        final long length = file.length();
454
455        // Build the new binary dictionary
456        final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0, length,
457                true /* useFullEditDistance */, null, mDictType, mIsUpdatable);
458
459        // Ensure all threads accessing the current dictionary have finished before
460        // swapping in the new one.
461        // TODO: Ensure multi-thread assignment of mBinaryDictionary.
462        final BinaryDictionary oldBinaryDictionary = mBinaryDictionary;
463        getExecutor(mFilename).executePrioritized(new Runnable() {
464            @Override
465            public void run() {
466                mBinaryDictionary = newBinaryDictionary;
467                if (oldBinaryDictionary != null) {
468                    oldBinaryDictionary.close();
469                }
470            }
471        });
472    }
473
474    /**
475     * Abstract method for checking if it is required to reload the dictionary before writing
476     * a binary dictionary.
477     */
478    abstract protected boolean needsToReloadBeforeWriting();
479
480    /**
481     * Writes a new binary dictionary based on the contents of the fusion dictionary.
482     */
483    private void writeBinaryDictionary() {
484        if (DEBUG) {
485            Log.d(TAG, "Generating binary dictionary: " + mFilename + " request="
486                    + mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime + " update="
487                    + mFilenameDictionaryTimeRecorder.mLastUpdateTime);
488        }
489        if (needsToReloadBeforeWriting()) {
490            mDictionaryWriter.clear();
491            loadDictionaryAsync();
492            mDictionaryWriter.write(mFilename, getHeaderAttributeMap());
493        } else {
494            if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
495                if (mBinaryDictionary == null || !mBinaryDictionary.isValidDictionary()) {
496                    final File file = new File(mContext.getFilesDir(), mFilename);
497                    BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
498                            DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
499                } else {
500                    if (mBinaryDictionary.needsToRunGC()) {
501                        mBinaryDictionary.flushWithGC();
502                    } else {
503                        mBinaryDictionary.flush();
504                    }
505                }
506            } else {
507                mDictionaryWriter.write(mFilename, getHeaderAttributeMap());
508            }
509        }
510    }
511
512    /**
513     * Marks that the dictionary is out of date and requires a reload.
514     *
515     * @param requiresRebuild Indicates that the source dictionary content has changed and a rebuild
516     *        of the binary file is required. If not true, the next reload process will only read
517     *        the current binary dictionary from file.
518     */
519    protected void setRequiresReload(final boolean requiresRebuild) {
520        final long time = SystemClock.uptimeMillis();
521        mPerInstanceDictionaryTimeRecorder.mLastUpdateRequestTime = time;
522        mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime = time;
523        if (DEBUG) {
524            Log.d(TAG, "Reload request: " + mFilename + ": request=" + time + " update="
525                    + mFilenameDictionaryTimeRecorder.mLastUpdateTime);
526        }
527    }
528
529    /**
530     * Reloads the dictionary if required.
531     */
532    public final void reloadDictionaryIfRequired() {
533        if (!isReloadRequired()) return;
534        reloadDictionary();
535    }
536
537    /**
538     * Returns whether a dictionary reload is required.
539     */
540    private boolean isReloadRequired() {
541        return mBinaryDictionary == null || mPerInstanceDictionaryTimeRecorder.isOutOfDate();
542    }
543
544    /**
545     * Reloads the dictionary. Access is controlled on a per dictionary file basis and supports
546     * concurrent calls from multiple instances that share the same dictionary file.
547     */
548    private final void reloadDictionary() {
549        // Ensure that only one thread attempts to read or write to the shared binary dictionary
550        // file at the same time.
551        getExecutor(mFilename).execute(new Runnable() {
552            @Override
553            public void run() {
554                final long time = SystemClock.uptimeMillis();
555                final boolean dictionaryFileExists = dictionaryFileExists();
556                if (mFilenameDictionaryTimeRecorder.isOutOfDate() || !dictionaryFileExists) {
557                    // If the shared dictionary file does not exist or is out of date, the first
558                    // instance that acquires the lock will generate a new one.
559                    if (hasContentChanged() || !dictionaryFileExists) {
560                        // If the source content has changed or the dictionary does not exist,
561                        // rebuild the binary dictionary. Empty dictionaries are supported (in the
562                        // case where loadDictionaryAsync() adds nothing) in order to provide a
563                        // uniform framework.
564                        mFilenameDictionaryTimeRecorder.mLastUpdateTime = time;
565                        writeBinaryDictionary();
566                        loadBinaryDictionary();
567                    } else {
568                        // If not, the reload request was unnecessary so revert
569                        // LastUpdateRequestTime to LastUpdateTime.
570                        mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime =
571                                mFilenameDictionaryTimeRecorder.mLastUpdateTime;
572                    }
573                } else if (mBinaryDictionary == null ||
574                        mPerInstanceDictionaryTimeRecorder.mLastUpdateTime
575                                < mFilenameDictionaryTimeRecorder.mLastUpdateTime) {
576                    // Otherwise, if the local dictionary is older than the shared dictionary, load
577                    // the shared dictionary.
578                    loadBinaryDictionary();
579                }
580                if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) {
581                    // Binary dictionary is not valid. Regenerate the dictionary file.
582                    mFilenameDictionaryTimeRecorder.mLastUpdateTime = time;
583                    writeBinaryDictionary();
584                    loadBinaryDictionary();
585                }
586                mPerInstanceDictionaryTimeRecorder.mLastUpdateTime = time;
587            }
588        });
589    }
590
591    // TODO: cache the file's existence so that we avoid doing a disk access each time.
592    private boolean dictionaryFileExists() {
593        final File file = new File(mContext.getFilesDir(), mFilename);
594        return file.exists();
595    }
596
597    /**
598     * Load the dictionary to memory.
599     */
600    protected void asyncLoadDictionaryToMemory() {
601        getExecutor(mFilename).executePrioritized(new Runnable() {
602            @Override
603            public void run() {
604                if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
605                    loadDictionaryAsync();
606                }
607            }
608        });
609    }
610
611    /**
612     * Generate binary dictionary using DictionaryWriter.
613     */
614    protected void asyncFlashAllBinaryDictionary() {
615        final Runnable newTask = new Runnable() {
616            @Override
617            public void run() {
618                writeBinaryDictionary();
619            }
620        };
621        final Runnable oldTask = mUnfinishedFlushingTask.getAndSet(newTask);
622        getExecutor(mFilename).replaceAndExecute(oldTask, newTask);
623    }
624
625    /**
626     * Time recorder for tracking whether the dictionary is out of date.
627     * Can be shared across multiple dictionary instances that access the same filename.
628     */
629    private static class DictionaryTimeRecorder {
630        private volatile long mLastUpdateTime = 0;
631        private volatile long mLastUpdateRequestTime = 0;
632
633        private boolean isOutOfDate() {
634            return (mLastUpdateRequestTime > mLastUpdateTime);
635        }
636    }
637
638    // TODO: Implement native binary methods once the dynamic dictionary implementation is done.
639    @UsedForTesting
640    public boolean isInDictionaryForTests(final String word) {
641        final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>();
642        getExecutor(mFilename).executePrioritized(new Runnable() {
643            @Override
644            public void run() {
645                if (mDictType == Dictionary.TYPE_USER_HISTORY) {
646                    if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
647                        holder.set(mBinaryDictionary.isValidWord(word));
648                    } else {
649                        holder.set(((DynamicPersonalizationDictionaryWriter) mDictionaryWriter)
650                                .isInDictionaryForTests(word));
651                    }
652                }
653            }
654        });
655        return holder.get(false, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
656    }
657
658    @UsedForTesting
659    public void shutdownExecutorForTests() {
660        getExecutor(mFilename).shutdown();
661    }
662
663    @UsedForTesting
664    public boolean isTerminatedForTests() {
665        return getExecutor(mFilename).isTerminated();
666    }
667}
668