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