diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java')
-rw-r--r-- | java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java | 671 |
1 files changed, 463 insertions, 208 deletions
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 887d657e9..99859decf 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -20,21 +20,21 @@ import android.content.Context; import android.os.SystemClock; import android.util.Log; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.makedict.BinaryDictInputOutput; import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.makedict.FusionDictionary; -import com.android.inputmethod.latin.makedict.FusionDictionary.Node; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; -import com.android.inputmethod.latin.makedict.UnsupportedFormatException; +import com.android.inputmethod.latin.personalization.DynamicPersonalizationDictionaryWriter; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.utils.AsyncResultHolder; +import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.PrioritizedSerialExecutor; import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; -import java.util.concurrent.locks.ReentrantLock; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; /** * Abstract base class for an expandable dictionary that can be created and updated dynamically @@ -52,19 +52,32 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { /** Whether to print debug output to log */ private static boolean DEBUG = false; + // TODO: Remove. + /** Whether to call binary dictionary dynamically updating methods. */ + public static boolean ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE = true; + + private static final int TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS = 100; + /** * The maximum length of a word in this dictionary. */ - protected static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH; + protected static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; + + private static final int DICTIONARY_FORMAT_VERSION = 3; + + private static final String SUPPORTS_DYNAMIC_UPDATE = + FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE; /** - * A static map of locks, each of which controls access to a single binary dictionary file. They - * ensure that only one instance can update the same dictionary at the same time. The key for - * this map is the filename and the value is the shared dictionary controller associated with - * that filename. + * A static map of time recorders, each of which records the time of accesses to a single binary + * dictionary file. The key for this map is the filename and the value is the shared dictionary + * time recorder associated with that filename. */ - private static final HashMap<String, DictionaryController> sSharedDictionaryControllers = - CollectionUtils.newHashMap(); + private static volatile ConcurrentHashMap<String, DictionaryTimeRecorder> + sFilenameDictionaryTimeRecorderMap = CollectionUtils.newConcurrentHashMap(); + + private static volatile ConcurrentHashMap<String, PrioritizedSerialExecutor> + sFilenameExecutorMap = CollectionUtils.newConcurrentHashMap(); /** The application context. */ protected final Context mContext; @@ -75,25 +88,34 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ private BinaryDictionary mBinaryDictionary; - /** The expandable fusion dictionary used to generate the binary dictionary. */ - private FusionDictionary mFusionDictionary; + // TODO: Remove and handle dictionaries in native code. + /** The in-memory dictionary used to generate the binary dictionary. */ + protected AbstractDictionaryWriter mDictionaryWriter; /** * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple * dictionary instances with the same filename is supported, with access controlled by - * DictionaryController. + * DictionaryTimeRecorder. */ private final String mFilename; - /** Controls access to the shared binary dictionary file across multiple instances. */ - private final DictionaryController mSharedDictionaryController; + /** Whether to support dynamically updating the dictionary */ + private final boolean mIsUpdatable; + + // TODO: remove, once dynamic operations is serialized + /** Records access to the shared binary dictionary file across multiple instances. */ + private final DictionaryTimeRecorder mFilenameDictionaryTimeRecorder; + + // TODO: remove, once dynamic operations is serialized + /** Records access to the local binary dictionary for this instance. */ + private final DictionaryTimeRecorder mPerInstanceDictionaryTimeRecorder = + new DictionaryTimeRecorder(); - /** Controls access to the local binary dictionary for this instance. */ - private final DictionaryController mLocalDictionaryController = new DictionaryController(); + /* A extension for a binary dictionary file. */ + public static final String DICT_FILE_EXTENSION = ".dict"; - private static final int BINARY_DICT_VERSION = 1; - private static final FormatSpec.FormatOptions FORMAT_OPTIONS = - new FormatSpec.FormatOptions(BINARY_DICT_VERSION); + private final AtomicReference<Runnable> mUnfinishedFlushingTask = + new AtomicReference<Runnable>(); /** * Abstract method for loading the unigrams and bigrams of a given dictionary in a background @@ -109,16 +131,45 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { protected abstract boolean hasContentChanged(); /** - * Gets the shared dictionary controller for the given filename. + * Gets the dictionary time recorder for the given filename. */ - private static synchronized DictionaryController getSharedDictionaryController( + private static DictionaryTimeRecorder getDictionaryTimeRecorder( String filename) { - DictionaryController controller = sSharedDictionaryControllers.get(filename); - if (controller == null) { - controller = new DictionaryController(); - sSharedDictionaryControllers.put(filename, controller); + DictionaryTimeRecorder recorder = sFilenameDictionaryTimeRecorderMap.get(filename); + if (recorder == null) { + synchronized(sFilenameDictionaryTimeRecorderMap) { + recorder = new DictionaryTimeRecorder(); + sFilenameDictionaryTimeRecorderMap.put(filename, recorder); + } + } + return recorder; + } + + /** + * Gets the executor for the given filename. + */ + private static PrioritizedSerialExecutor getExecutor(final String filename) { + PrioritizedSerialExecutor executor = sFilenameExecutorMap.get(filename); + if (executor == null) { + synchronized(sFilenameExecutorMap) { + executor = new PrioritizedSerialExecutor(); + sFilenameExecutorMap.put(filename, executor); + } + } + return executor; + } + + private static AbstractDictionaryWriter getDictionaryWriter(final Context context, + final String dictType, final boolean isDynamicPersonalizationDictionary) { + if (isDynamicPersonalizationDictionary) { + if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + return null; + } else { + return new DynamicPersonalizationDictionaryWriter(context, dictType); + } + } else { + return new DictionaryWriter(context, dictType); } - return controller; } /** @@ -128,19 +179,23 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * @param filename The filename for this binary dictionary. Multiple dictionaries with the same * filename is supported. * @param dictType the dictionary type, as a human-readable string + * @param isUpdatable whether to support dynamically updating the dictionary. Please note that + * dynamic dictionary has negative effects on memory space and computation time. */ - public ExpandableBinaryDictionary( - final Context context, final String filename, final String dictType) { + public ExpandableBinaryDictionary(final Context context, final String filename, + final String dictType, final boolean isUpdatable) { super(dictType); mFilename = filename; mContext = context; + mIsUpdatable = isUpdatable; mBinaryDictionary = null; - mSharedDictionaryController = getSharedDictionaryController(filename); - clearFusionDictionary(); + mFilenameDictionaryTimeRecorder = getDictionaryTimeRecorder(filename); + // Currently, only dynamic personalization dictionary is updatable. + mDictionaryWriter = getDictionaryWriter(context, dictType, isUpdatable); } protected static String getFilenameWithLocale(final String name, final String localeStr) { - return name + "." + localeStr + ".dict"; + return name + "." + localeStr + DICT_FILE_EXTENSION; } /** @@ -148,89 +203,218 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ @Override public void close() { + getExecutor(mFilename).execute(new Runnable() { + @Override + public void run() { + if (mBinaryDictionary!= null) { + mBinaryDictionary.close(); + mBinaryDictionary = null; + } + if (mDictionaryWriter != null) { + mDictionaryWriter.close(); + } + } + }); + } + + protected void closeBinaryDictionary() { // Ensure that no other threads are accessing the local binary dictionary. - mLocalDictionaryController.lock(); - try { - if (mBinaryDictionary != null) { - mBinaryDictionary.close(); - mBinaryDictionary = null; + getExecutor(mFilename).execute(new Runnable() { + @Override + public void run() { + if (mBinaryDictionary != null) { + mBinaryDictionary.close(); + mBinaryDictionary = null; + } } - } finally { - mLocalDictionaryController.unlock(); - } + }); + } + + protected Map<String, String> getHeaderAttributeMap() { + HashMap<String, String> attributeMap = new HashMap<String, String>(); + attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE, + SUPPORTS_DYNAMIC_UPDATE); + return attributeMap; + } + + protected void clear() { + getExecutor(mFilename).execute(new Runnable() { + @Override + public void run() { + if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE && mDictionaryWriter == null) { + mBinaryDictionary.close(); + final File file = new File(mContext.getFilesDir(), mFilename); + BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), + DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); + } else { + mDictionaryWriter.clear(); + } + } + }); } /** - * Clears the fusion dictionary on the Java side. Note: Does not modify the binary dictionary on - * the native side. + * Adds a word unigram to the dictionary. Used for loading a dictionary. */ - public void clearFusionDictionary() { - final HashMap<String, String> attributes = CollectionUtils.newHashMap(); - mFusionDictionary = new FusionDictionary(new Node(), - new FusionDictionary.DictionaryOptions(attributes, false, false)); + protected void addWord(final String word, final String shortcutTarget, + final int frequency, final boolean isNotAWord) { + mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord); } /** - * Adds a word unigram to the fusion dictionary. Call updateBinaryDictionary when all changes - * are done to update the binary dictionary. + * Adds a word bigram in the dictionary. Used for loading a dictionary. */ - // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries, - // considering performance regression. - protected void addWord(final String word, final String shortcutTarget, final int frequency, - final boolean isNotAWord) { - if (shortcutTarget == null) { - mFusionDictionary.add(word, frequency, null, isNotAWord); - } else { - // TODO: Do this in the subclass, with this class taking an arraylist. - final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList(); - shortcutTargets.add(new WeightedString(shortcutTarget, frequency)); - mFusionDictionary.add(word, frequency, shortcutTargets, isNotAWord); + protected void addBigram(final String prevWord, final String word, final int frequency, + final long lastModifiedTime) { + mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */, + lastModifiedTime); + } + + /** + * Dynamically adds a word unigram to the dictionary. May overwrite an existing entry. + */ + protected void addWordDynamically(final String word, final String shortcutTarget, + final int frequency, final boolean isNotAWord) { + if (!mIsUpdatable) { + Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename); + return; } + + getExecutor(mFilename).execute(new Runnable() { + @Override + public void run() { + if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + mBinaryDictionary.addUnigramWord(word, frequency); + } else { + // TODO: Remove. + mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord); + } + } + }); } /** - * Sets a word bigram in the fusion dictionary. Call updateBinaryDictionary when all changes are - * done to update the binary dictionary. + * Dynamically adds a word bigram in the dictionary. May overwrite an existing entry. */ - // TODO: Create "cache dictionary" to cache fresh bigrams for frequently updated dictionaries, - // considering performance regression. - protected void setBigram(final String prevWord, final String word, final int frequency) { - mFusionDictionary.setBigram(prevWord, word, frequency); + protected void addBigramDynamically(final String word0, final String word1, + final int frequency, final boolean isValid) { + if (!mIsUpdatable) { + Log.w(TAG, "addBigramDynamically is called for non-updatable dictionary: " + + mFilename); + return; + } + + getExecutor(mFilename).execute(new Runnable() { + @Override + public void run() { + if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + mBinaryDictionary.addBigramWords(word0, word1, frequency); + } else { + // TODO: Remove. + mDictionaryWriter.addBigramWords(word0, word1, frequency, isValid, + 0 /* lastTouchedTime */); + } + } + }); + } + + /** + * Dynamically remove a word bigram in the dictionary. + */ + protected void removeBigramDynamically(final String word0, final String word1) { + if (!mIsUpdatable) { + Log.w(TAG, "removeBigramDynamically is called for non-updatable dictionary: " + + mFilename); + return; + } + + getExecutor(mFilename).execute(new Runnable() { + @Override + public void run() { + if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + mBinaryDictionary.removeBigramWords(word0, word1); + } else { + // TODO: Remove. + mDictionaryWriter.removeBigramWords(word0, word1); + } + } + }); } @Override - public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, + public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords) { - asyncReloadDictionaryIfRequired(); - if (mLocalDictionaryController.tryLock()) { - try { - if (mBinaryDictionary != null) { - return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo, - blockOffensiveWords); + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final int sessionId) { + reloadDictionaryIfRequired(); + final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); + final AsyncResultHolder<ArrayList<SuggestedWordInfo>> holder = + new AsyncResultHolder<ArrayList<SuggestedWordInfo>>(); + getExecutor(mFilename).executePrioritized(new Runnable() { + @Override + public void run() { + if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + if (mBinaryDictionary == null) { + holder.set(null); + return; + } + final ArrayList<SuggestedWordInfo> binarySuggestion = + mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord, + proximityInfo, blockOffensiveWords, additionalFeaturesOptions, + sessionId); + holder.set(binarySuggestion); + } else { + final ArrayList<SuggestedWordInfo> inMemDictSuggestion = + composer.isBatchMode() ? null : + mDictionaryWriter.getSuggestionsWithSessionId(composer, + prevWord, proximityInfo, blockOffensiveWords, + additionalFeaturesOptions, sessionId); + // TODO: Remove checking mIsUpdatable and use native suggestion. + if (mBinaryDictionary != null && !mIsUpdatable) { + final ArrayList<SuggestedWordInfo> binarySuggestion = + mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord, + proximityInfo, blockOffensiveWords, + additionalFeaturesOptions, sessionId); + if (inMemDictSuggestion == null) { + holder.set(binarySuggestion); + } else if (binarySuggestion == null) { + holder.set(inMemDictSuggestion); + } else { + binarySuggestion.addAll(inMemDictSuggestion); + holder.set(binarySuggestion); + } + } else { + holder.set(inMemDictSuggestion); + } } - } finally { - mLocalDictionaryController.unlock(); } - } - return null; + }); + return holder.get(null, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS); + } + + @Override + public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, + final String prevWord, final ProximityInfo proximityInfo, + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { + return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords, + additionalFeaturesOptions, 0 /* sessionId */); } @Override public boolean isValidWord(final String word) { - asyncReloadDictionaryIfRequired(); + reloadDictionaryIfRequired(); return isValidWordInner(word); } protected boolean isValidWordInner(final String word) { - if (mLocalDictionaryController.tryLock()) { - try { - return isValidWordLocked(word); - } finally { - mLocalDictionaryController.unlock(); + final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>(); + getExecutor(mFilename).executePrioritized(new Runnable() { + @Override + public void run() { + holder.set(isValidWordLocked(word)); } - } - return false; + }); + return holder.get(false, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS); } protected boolean isValidWordLocked(final String word) { @@ -238,22 +422,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { return mBinaryDictionary.isValidWord(word); } - protected boolean isValidBigram(final String word1, final String word2) { - if (mBinaryDictionary == null) return false; - return mBinaryDictionary.isValidBigram(word1, word2); - } - - protected boolean isValidBigramInner(final String word1, final String word2) { - if (mLocalDictionaryController.tryLock()) { - try { - return isValidBigramLocked(word1, word2); - } finally { - mLocalDictionaryController.unlock(); - } - } - return false; - } - protected boolean isValidBigramLocked(final String word1, final String word2) { if (mBinaryDictionary == null) return false; return mBinaryDictionary.isValidBigram(word1, word2); @@ -264,19 +432,19 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * dictionary exists, this method will generate one. */ protected void loadDictionary() { - mLocalDictionaryController.mLastUpdateRequestTime = SystemClock.uptimeMillis(); - asyncReloadDictionaryIfRequired(); + mPerInstanceDictionaryTimeRecorder.mLastUpdateRequestTime = SystemClock.uptimeMillis(); + reloadDictionaryIfRequired(); } /** * Loads the current binary dictionary from internal storage. Assumes the dictionary file * exists. */ - protected void loadBinaryDictionary() { + private void loadBinaryDictionary() { if (DEBUG) { Log.d(TAG, "Loading binary dictionary: " + mFilename + " request=" - + mSharedDictionaryController.mLastUpdateRequestTime + " update=" - + mSharedDictionaryController.mLastUpdateTime); + + mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime + " update=" + + mFilenameDictionaryTimeRecorder.mLastUpdateTime); } final File file = new File(mContext.getFilesDir(), mFilename); @@ -285,55 +453,57 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { // Build the new binary dictionary final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0, length, - true /* useFullEditDistance */, null, mDictType); - - if (mBinaryDictionary != null) { - // Ensure all threads accessing the current dictionary have finished before swapping in - // the new one. - final BinaryDictionary oldBinaryDictionary = mBinaryDictionary; - mLocalDictionaryController.lock(); - mBinaryDictionary = newBinaryDictionary; - mLocalDictionaryController.unlock(); - oldBinaryDictionary.close(); - } else { - mBinaryDictionary = newBinaryDictionary; - } + true /* useFullEditDistance */, null, mDictType, mIsUpdatable); + + // Ensure all threads accessing the current dictionary have finished before + // swapping in the new one. + // TODO: Ensure multi-thread assignment of mBinaryDictionary. + final BinaryDictionary oldBinaryDictionary = mBinaryDictionary; + getExecutor(mFilename).executePrioritized(new Runnable() { + @Override + public void run() { + mBinaryDictionary = newBinaryDictionary; + if (oldBinaryDictionary != null) { + oldBinaryDictionary.close(); + } + } + }); } /** - * Generates and writes a new binary dictionary based on the contents of the fusion dictionary. + * Abstract method for checking if it is required to reload the dictionary before writing + * a binary dictionary. */ - private void generateBinaryDictionary() { + abstract protected boolean needsToReloadBeforeWriting(); + + /** + * Writes a new binary dictionary based on the contents of the fusion dictionary. + */ + private void writeBinaryDictionary() { if (DEBUG) { Log.d(TAG, "Generating binary dictionary: " + mFilename + " request=" - + mSharedDictionaryController.mLastUpdateRequestTime + " update=" - + mSharedDictionaryController.mLastUpdateTime); + + mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime + " update=" + + mFilenameDictionaryTimeRecorder.mLastUpdateTime); } - - loadDictionaryAsync(); - - final String tempFileName = mFilename + ".temp"; - final File file = new File(mContext.getFilesDir(), mFilename); - final File tempFile = new File(mContext.getFilesDir(), tempFileName); - FileOutputStream out = null; - try { - out = new FileOutputStream(tempFile); - BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, FORMAT_OPTIONS); - out.flush(); - out.close(); - tempFile.renameTo(file); - clearFusionDictionary(); - } catch (IOException e) { - Log.e(TAG, "IO exception while writing file", e); - } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported format", e); - } finally { - if (out != null) { - try { - out.close(); - } catch (IOException e) { - // ignore + if (needsToReloadBeforeWriting()) { + mDictionaryWriter.clear(); + loadDictionaryAsync(); + mDictionaryWriter.write(mFilename); + } else { + if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + if (mBinaryDictionary == null || !mBinaryDictionary.isValidDictionary()) { + final File file = new File(mContext.getFilesDir(), mFilename); + BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), + DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); + } else { + if (mBinaryDictionary.needsToRunGC()) { + mBinaryDictionary.flushWithGC(); + } else { + mBinaryDictionary.flush(); + } } + } else { + mDictionaryWriter.write(mFilename); } } } @@ -347,77 +517,74 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ protected void setRequiresReload(final boolean requiresRebuild) { final long time = SystemClock.uptimeMillis(); - mLocalDictionaryController.mLastUpdateRequestTime = time; - mSharedDictionaryController.mLastUpdateRequestTime = time; + mPerInstanceDictionaryTimeRecorder.mLastUpdateRequestTime = time; + mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime = time; if (DEBUG) { Log.d(TAG, "Reload request: " + mFilename + ": request=" + time + " update=" - + mSharedDictionaryController.mLastUpdateTime); - } - } - - /** - * Reloads the dictionary if required. Reload will occur asynchronously in a separate thread. - */ - void asyncReloadDictionaryIfRequired() { - if (!isReloadRequired()) return; - if (DEBUG) { - Log.d(TAG, "Starting AsyncReloadDictionaryTask: " + mFilename); + + mFilenameDictionaryTimeRecorder.mLastUpdateTime); } - new AsyncReloadDictionaryTask().start(); } /** * Reloads the dictionary if required. */ - protected final void syncReloadDictionaryIfRequired() { + public final void reloadDictionaryIfRequired() { if (!isReloadRequired()) return; - syncReloadDictionaryInternal(); + reloadDictionary(); } /** * Returns whether a dictionary reload is required. */ private boolean isReloadRequired() { - return mBinaryDictionary == null || mLocalDictionaryController.isOutOfDate(); + return mBinaryDictionary == null || mPerInstanceDictionaryTimeRecorder.isOutOfDate(); } /** * Reloads the dictionary. Access is controlled on a per dictionary file basis and supports * concurrent calls from multiple instances that share the same dictionary file. */ - private final void syncReloadDictionaryInternal() { + private final void reloadDictionary() { // Ensure that only one thread attempts to read or write to the shared binary dictionary // file at the same time. - mSharedDictionaryController.lock(); - try { - final long time = SystemClock.uptimeMillis(); - final boolean dictionaryFileExists = dictionaryFileExists(); - if (mSharedDictionaryController.isOutOfDate() || !dictionaryFileExists) { - // If the shared dictionary file does not exist or is out of date, the first - // instance that acquires the lock will generate a new one. - if (hasContentChanged() || !dictionaryFileExists) { - // If the source content has changed or the dictionary does not exist, rebuild - // the binary dictionary. Empty dictionaries are supported (in the case where - // loadDictionaryAsync() adds nothing) in order to provide a uniform framework. - mSharedDictionaryController.mLastUpdateTime = time; - generateBinaryDictionary(); + getExecutor(mFilename).execute(new Runnable() { + @Override + public void run() { + final long time = SystemClock.uptimeMillis(); + final boolean dictionaryFileExists = dictionaryFileExists(); + if (mFilenameDictionaryTimeRecorder.isOutOfDate() || !dictionaryFileExists) { + // If the shared dictionary file does not exist or is out of date, the first + // instance that acquires the lock will generate a new one. + if (hasContentChanged() || !dictionaryFileExists) { + // If the source content has changed or the dictionary does not exist, + // rebuild the binary dictionary. Empty dictionaries are supported (in the + // case where loadDictionaryAsync() adds nothing) in order to provide a + // uniform framework. + mFilenameDictionaryTimeRecorder.mLastUpdateTime = time; + writeBinaryDictionary(); + loadBinaryDictionary(); + } else { + // If not, the reload request was unnecessary so revert + // LastUpdateRequestTime to LastUpdateTime. + mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime = + mFilenameDictionaryTimeRecorder.mLastUpdateTime; + } + } else if (mBinaryDictionary == null || + mPerInstanceDictionaryTimeRecorder.mLastUpdateTime + < mFilenameDictionaryTimeRecorder.mLastUpdateTime) { + // Otherwise, if the local dictionary is older than the shared dictionary, load + // the shared dictionary. + loadBinaryDictionary(); + } + if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) { + // Binary dictionary is not valid. Regenerate the dictionary file. + mFilenameDictionaryTimeRecorder.mLastUpdateTime = time; + writeBinaryDictionary(); loadBinaryDictionary(); - } else { - // If not, the reload request was unnecessary so revert LastUpdateRequestTime - // to LastUpdateTime. - mSharedDictionaryController.mLastUpdateRequestTime = - mSharedDictionaryController.mLastUpdateTime; } - } else if (mBinaryDictionary == null || mLocalDictionaryController.mLastUpdateTime - < mSharedDictionaryController.mLastUpdateTime) { - // Otherwise, if the local dictionary is older than the shared dictionary, load the - // shared dictionary. - loadBinaryDictionary(); + mPerInstanceDictionaryTimeRecorder.mLastUpdateTime = time; } - mLocalDictionaryController.mLastUpdateTime = time; - } finally { - mSharedDictionaryController.unlock(); - } + }); } // TODO: cache the file's existence so that we avoid doing a disk access each time. @@ -427,21 +594,38 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } /** - * Thread class for asynchronously reloading and rewriting the binary dictionary. + * Load the dictionary to memory. */ - private class AsyncReloadDictionaryTask extends Thread { - @Override - public void run() { - syncReloadDictionaryInternal(); - } + protected void asyncLoadDictionaryToMemory() { + getExecutor(mFilename).executePrioritized(new Runnable() { + @Override + public void run() { + if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + loadDictionaryAsync(); + } + } + }); + } + + /** + * Generate binary dictionary using DictionaryWriter. + */ + protected void asyncFlashAllBinaryDictionary() { + final Runnable newTask = new Runnable() { + @Override + public void run() { + writeBinaryDictionary(); + } + }; + final Runnable oldTask = mUnfinishedFlushingTask.getAndSet(newTask); + getExecutor(mFilename).replaceAndExecute(oldTask, newTask); } /** - * Lock for controlling access to a given binary dictionary and for tracking whether the - * dictionary is out of date. Can be shared across multiple dictionary instances that access the - * same filename. + * Time recorder for tracking whether the dictionary is out of date. + * Can be shared across multiple dictionary instances that access the same filename. */ - private static class DictionaryController extends ReentrantLock { + private static class DictionaryTimeRecorder { private volatile long mLastUpdateTime = 0; private volatile long mLastUpdateRequestTime = 0; @@ -449,4 +633,75 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { return (mLastUpdateRequestTime > mLastUpdateTime); } } + + /** + * Dynamically adds a word unigram to the dictionary for testing with blocking-lock. + */ + @UsedForTesting + protected void addWordDynamicallyForTests(final String word, final String shortcutTarget, + final int frequency, final boolean isNotAWord) { + getExecutor(mFilename).executePrioritized(new Runnable() { + @Override + public void run() { + addWordDynamically(word, shortcutTarget, frequency, isNotAWord); + } + }); + } + + /** + * Dynamically adds a word bigram in the dictionary for testing with blocking-lock. + */ + @UsedForTesting + protected void addBigramDynamicallyForTests(final String word0, final String word1, + final int frequency, final boolean isValid) { + getExecutor(mFilename).executePrioritized(new Runnable() { + @Override + public void run() { + addBigramDynamically(word0, word1, frequency, isValid); + } + }); + } + + /** + * Dynamically remove a word bigram in the dictionary for testing with blocking-lock. + */ + @UsedForTesting + protected void removeBigramDynamicallyForTests(final String word0, final String word1) { + getExecutor(mFilename).executePrioritized(new Runnable() { + @Override + public void run() { + removeBigramDynamically(word0, word1); + } + }); + } + + // TODO: Implement native binary methods once the dynamic dictionary implementation is done. + @UsedForTesting + public boolean isInDictionaryForTests(final String word) { + final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>(); + getExecutor(mFilename).executePrioritized(new Runnable() { + @Override + public void run() { + if (mDictType == Dictionary.TYPE_USER_HISTORY) { + if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { + holder.set(mBinaryDictionary.isValidWord(word)); + } else { + holder.set(((DynamicPersonalizationDictionaryWriter) mDictionaryWriter) + .isInDictionaryForTests(word)); + } + } + } + }); + return holder.get(false, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS); + } + + @UsedForTesting + public void shutdownExecutorForTests() { + getExecutor(mFilename).shutdown(); + } + + @UsedForTesting + public boolean isTerminatedForTests() { + return getExecutor(mFilename).isTerminated(); + } } |