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