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