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