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