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.util.Log; 21 22 import com.android.inputmethod.annotations.UsedForTesting; 23 import com.android.inputmethod.keyboard.ProximityInfo; 24 import com.android.inputmethod.latin.makedict.DictionaryHeader; 25 import com.android.inputmethod.latin.makedict.FormatSpec; 26 import com.android.inputmethod.latin.makedict.UnsupportedFormatException; 27 import com.android.inputmethod.latin.makedict.WordProperty; 28 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 29 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; 30 import com.android.inputmethod.latin.utils.CombinedFormatUtils; 31 import com.android.inputmethod.latin.utils.DistracterFilter; 32 import com.android.inputmethod.latin.utils.ExecutorUtils; 33 import com.android.inputmethod.latin.utils.FileUtils; 34 import com.android.inputmethod.latin.utils.LanguageModelParam; 35 36 import java.io.File; 37 import java.util.ArrayList; 38 import java.util.HashMap; 39 import java.util.Locale; 40 import java.util.Map; 41 import java.util.concurrent.Callable; 42 import java.util.concurrent.CountDownLatch; 43 import java.util.concurrent.TimeUnit; 44 import java.util.concurrent.atomic.AtomicBoolean; 45 import java.util.concurrent.locks.Lock; 46 import java.util.concurrent.locks.ReentrantReadWriteLock; 47 48 /** 49 * Abstract base class for an expandable dictionary that can be created and updated dynamically 50 * during runtime. When updated it automatically generates a new binary dictionary to handle future 51 * queries in native code. This binary dictionary is written to internal storage. 52 */ 53 abstract public class ExpandableBinaryDictionary extends Dictionary { 54 private static final boolean DEBUG = false; 55 56 /** Used for Log actions from this class */ 57 private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName(); 58 59 /** Whether to print debug output to log */ 60 private static final boolean DBG_STRESS_TEST = false; 61 62 private static final int TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS = 100; 63 64 private static final int DEFAULT_MAX_UNIGRAM_COUNT = 10000; 65 private static final int DEFAULT_MAX_BIGRAM_COUNT = 10000; 66 67 /** 68 * The maximum length of a word in this dictionary. 69 */ 70 protected static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; 71 72 private static final int DICTIONARY_FORMAT_VERSION = FormatSpec.VERSION4; 73 74 /** The application context. */ 75 protected final Context mContext; 76 77 /** 78 * The binary dictionary generated dynamically from the fusion dictionary. This is used to 79 * answer unigram and bigram queries. 80 */ 81 private BinaryDictionary mBinaryDictionary; 82 83 /** 84 * The name of this dictionary, used as a part of the filename for storing the binary 85 * dictionary. 86 */ 87 private final String mDictName; 88 89 /** Dictionary locale */ 90 private final Locale mLocale; 91 92 /** Dictionary file */ 93 private final File mDictFile; 94 95 /** Indicates whether a task for reloading the dictionary has been scheduled. */ 96 private final AtomicBoolean mIsReloading; 97 98 /** Indicates whether the current dictionary needs to be recreated. */ 99 private boolean mNeedsToRecreate; 100 101 private final ReentrantReadWriteLock mLock; 102 103 private Map<String, String> mAdditionalAttributeMap = null; 104 105 /* A extension for a binary dictionary file. */ 106 protected static final String DICT_FILE_EXTENSION = ".dict"; 107 108 /** 109 * Abstract method for loading initial contents of a given dictionary. 110 */ loadInitialContentsLocked()111 protected abstract void loadInitialContentsLocked(); 112 matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion)113 private boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) { 114 return formatVersion == FormatSpec.VERSION4; 115 } 116 needsToMigrateDictionary(final int formatVersion)117 private boolean needsToMigrateDictionary(final int formatVersion) { 118 // When we bump up the dictionary format version, the old version should be added to here 119 // for supporting migration. Note that native code has to support reading such formats. 120 return formatVersion == FormatSpec.VERSION4_ONLY_FOR_TESTING; 121 } 122 isValidDictionaryLocked()123 public boolean isValidDictionaryLocked() { 124 return mBinaryDictionary.isValidDictionary(); 125 } 126 127 /** 128 * Creates a new expandable binary dictionary. 129 * 130 * @param context The application context of the parent. 131 * @param dictName The name of the dictionary. Multiple instances with the same 132 * name is supported. 133 * @param locale the dictionary locale. 134 * @param dictType the dictionary type, as a human-readable string 135 * @param dictFile dictionary file path. if null, use default dictionary path based on 136 * dictionary type. 137 */ ExpandableBinaryDictionary(final Context context, final String dictName, final Locale locale, final String dictType, final File dictFile)138 public ExpandableBinaryDictionary(final Context context, final String dictName, 139 final Locale locale, final String dictType, final File dictFile) { 140 super(dictType); 141 mDictName = dictName; 142 mContext = context; 143 mLocale = locale; 144 mDictFile = getDictFile(context, dictName, dictFile); 145 mBinaryDictionary = null; 146 mIsReloading = new AtomicBoolean(); 147 mNeedsToRecreate = false; 148 mLock = new ReentrantReadWriteLock(); 149 } 150 getDictFile(final Context context, final String dictName, final File dictFile)151 public static File getDictFile(final Context context, final String dictName, 152 final File dictFile) { 153 return (dictFile != null) ? dictFile 154 : new File(context.getFilesDir(), dictName + DICT_FILE_EXTENSION); 155 } 156 getDictName(final String name, final Locale locale, final File dictFile)157 public static String getDictName(final String name, final Locale locale, 158 final File dictFile) { 159 return dictFile != null ? dictFile.getName() : name + "." + locale.toString(); 160 } 161 asyncExecuteTaskWithWriteLock(final Runnable task)162 private void asyncExecuteTaskWithWriteLock(final Runnable task) { 163 asyncExecuteTaskWithLock(mLock.writeLock(), task); 164 } 165 asyncExecuteTaskWithLock(final Lock lock, final Runnable task)166 private void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) { 167 asyncPreCheckAndExecuteTaskWithLock(lock, null /* preCheckTask */, task); 168 } 169 asyncPreCheckAndExecuteTaskWithWriteLock( final Callable<Boolean> preCheckTask, final Runnable task)170 private void asyncPreCheckAndExecuteTaskWithWriteLock( 171 final Callable<Boolean> preCheckTask, final Runnable task) { 172 asyncPreCheckAndExecuteTaskWithLock(mLock.writeLock(), preCheckTask, task); 173 174 } 175 176 // Execute task with lock when the result of preCheckTask is true or preCheckTask is null. asyncPreCheckAndExecuteTaskWithLock(final Lock lock, final Callable<Boolean> preCheckTask, final Runnable task)177 private void asyncPreCheckAndExecuteTaskWithLock(final Lock lock, 178 final Callable<Boolean> preCheckTask, final Runnable task) { 179 ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { 180 @Override 181 public void run() { 182 if (preCheckTask != null) { 183 try { 184 if (!preCheckTask.call().booleanValue()) { 185 return; 186 } 187 } catch (final Exception e) { 188 Log.e(TAG, "The pre check task throws an exception.", e); 189 return; 190 } 191 } 192 lock.lock(); 193 try { 194 task.run(); 195 } finally { 196 lock.unlock(); 197 } 198 } 199 }); 200 } 201 202 /** 203 * Closes and cleans up the binary dictionary. 204 */ 205 @Override close()206 public void close() { 207 asyncExecuteTaskWithWriteLock(new Runnable() { 208 @Override 209 public void run() { 210 if (mBinaryDictionary != null) { 211 mBinaryDictionary.close(); 212 mBinaryDictionary = null; 213 } 214 } 215 }); 216 } 217 getHeaderAttributeMap()218 protected Map<String, String> getHeaderAttributeMap() { 219 HashMap<String, String> attributeMap = new HashMap<>(); 220 if (mAdditionalAttributeMap != null) { 221 attributeMap.putAll(mAdditionalAttributeMap); 222 } 223 attributeMap.put(DictionaryHeader.DICTIONARY_ID_KEY, mDictName); 224 attributeMap.put(DictionaryHeader.DICTIONARY_LOCALE_KEY, mLocale.toString()); 225 attributeMap.put(DictionaryHeader.DICTIONARY_VERSION_KEY, 226 String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))); 227 attributeMap.put(DictionaryHeader.MAX_UNIGRAM_COUNT_KEY, 228 String.valueOf(DEFAULT_MAX_UNIGRAM_COUNT)); 229 attributeMap.put(DictionaryHeader.MAX_BIGRAM_COUNT_KEY, 230 String.valueOf(DEFAULT_MAX_BIGRAM_COUNT)); 231 return attributeMap; 232 } 233 removeBinaryDictionary()234 private void removeBinaryDictionary() { 235 asyncExecuteTaskWithWriteLock(new Runnable() { 236 @Override 237 public void run() { 238 removeBinaryDictionaryLocked(); 239 } 240 }); 241 } 242 removeBinaryDictionaryLocked()243 private void removeBinaryDictionaryLocked() { 244 if (mBinaryDictionary != null) { 245 mBinaryDictionary.close(); 246 } 247 if (mDictFile.exists() && !FileUtils.deleteRecursively(mDictFile)) { 248 Log.e(TAG, "Can't remove a file: " + mDictFile.getName()); 249 } 250 mBinaryDictionary = null; 251 } 252 openBinaryDictionaryLocked()253 private void openBinaryDictionaryLocked() { 254 mBinaryDictionary = new BinaryDictionary( 255 mDictFile.getAbsolutePath(), 0 /* offset */, mDictFile.length(), 256 true /* useFullEditDistance */, mLocale, mDictType, true /* isUpdatable */); 257 } 258 createOnMemoryBinaryDictionaryLocked()259 private void createOnMemoryBinaryDictionaryLocked() { 260 mBinaryDictionary = new BinaryDictionary( 261 mDictFile.getAbsolutePath(), true /* useFullEditDistance */, mLocale, mDictType, 262 DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); 263 } 264 clear()265 public void clear() { 266 asyncExecuteTaskWithWriteLock(new Runnable() { 267 @Override 268 public void run() { 269 removeBinaryDictionaryLocked(); 270 createOnMemoryBinaryDictionaryLocked(); 271 } 272 }); 273 } 274 275 /** 276 * Check whether GC is needed and run GC if required. 277 */ runGCIfRequired(final boolean mindsBlockByGC)278 protected void runGCIfRequired(final boolean mindsBlockByGC) { 279 asyncExecuteTaskWithWriteLock(new Runnable() { 280 @Override 281 public void run() { 282 if (mBinaryDictionary == null) { 283 return; 284 } 285 runGCIfRequiredLocked(mindsBlockByGC); 286 } 287 }); 288 } 289 runGCIfRequiredLocked(final boolean mindsBlockByGC)290 protected void runGCIfRequiredLocked(final boolean mindsBlockByGC) { 291 if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) { 292 mBinaryDictionary.flushWithGC(); 293 } 294 } 295 296 /** 297 * Adds unigram information of a word to the dictionary. May overwrite an existing entry. 298 */ addUnigramEntryWithCheckingDistracter(final String word, final int frequency, final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, final boolean isBlacklisted, final int timestamp, final DistracterFilter distracterFilter)299 public void addUnigramEntryWithCheckingDistracter(final String word, final int frequency, 300 final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, 301 final boolean isBlacklisted, final int timestamp, 302 final DistracterFilter distracterFilter) { 303 reloadDictionaryIfRequired(); 304 asyncPreCheckAndExecuteTaskWithWriteLock( 305 new Callable<Boolean>() { 306 @Override 307 public Boolean call() throws Exception { 308 return !distracterFilter.isDistracterToWordsInDictionaries( 309 PrevWordsInfo.EMPTY_PREV_WORDS_INFO, word, mLocale); 310 } 311 }, 312 new Runnable() { 313 @Override 314 public void run() { 315 if (mBinaryDictionary == null) { 316 return; 317 } 318 runGCIfRequiredLocked(true /* mindsBlockByGC */); 319 addUnigramLocked(word, frequency, shortcutTarget, shortcutFreq, 320 isNotAWord, isBlacklisted, timestamp); 321 } 322 }); 323 } 324 addUnigramLocked(final String word, final int frequency, final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, final boolean isBlacklisted, final int timestamp)325 protected void addUnigramLocked(final String word, final int frequency, 326 final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, 327 final boolean isBlacklisted, final int timestamp) { 328 if (!mBinaryDictionary.addUnigramEntry(word, frequency, shortcutTarget, shortcutFreq, 329 false /* isBeginningOfSentence */, isNotAWord, isBlacklisted, timestamp)) { 330 Log.e(TAG, "Cannot add unigram entry. word: " + word); 331 } 332 } 333 334 /** 335 * Dynamically remove the unigram entry from the dictionary. 336 */ removeUnigramEntryDynamically(final String word)337 public void removeUnigramEntryDynamically(final String word) { 338 reloadDictionaryIfRequired(); 339 asyncExecuteTaskWithWriteLock(new Runnable() { 340 @Override 341 public void run() { 342 if (mBinaryDictionary == null) { 343 return; 344 } 345 runGCIfRequiredLocked(true /* mindsBlockByGC */); 346 if (!mBinaryDictionary.removeUnigramEntry(word)) { 347 if (DEBUG) { 348 Log.i(TAG, "Cannot remove unigram entry: " + word); 349 } 350 } 351 } 352 }); 353 } 354 355 /** 356 * Adds n-gram information of a word to the dictionary. May overwrite an existing entry. 357 */ addNgramEntry(final PrevWordsInfo prevWordsInfo, final String word, final int frequency, final int timestamp)358 public void addNgramEntry(final PrevWordsInfo prevWordsInfo, final String word, 359 final int frequency, final int timestamp) { 360 reloadDictionaryIfRequired(); 361 asyncExecuteTaskWithWriteLock(new Runnable() { 362 @Override 363 public void run() { 364 if (mBinaryDictionary == null) { 365 return; 366 } 367 runGCIfRequiredLocked(true /* mindsBlockByGC */); 368 addNgramEntryLocked(prevWordsInfo, word, frequency, timestamp); 369 } 370 }); 371 } 372 addNgramEntryLocked(final PrevWordsInfo prevWordsInfo, final String word, final int frequency, final int timestamp)373 protected void addNgramEntryLocked(final PrevWordsInfo prevWordsInfo, final String word, 374 final int frequency, final int timestamp) { 375 if (!mBinaryDictionary.addNgramEntry(prevWordsInfo, word, frequency, timestamp)) { 376 if (DEBUG) { 377 Log.i(TAG, "Cannot add n-gram entry."); 378 Log.i(TAG, " PrevWordsInfo: " + prevWordsInfo + ", word: " + word); 379 } 380 } 381 } 382 383 /** 384 * Dynamically remove the n-gram entry in the dictionary. 385 */ 386 @UsedForTesting removeNgramDynamically(final PrevWordsInfo prevWordsInfo, final String word)387 public void removeNgramDynamically(final PrevWordsInfo prevWordsInfo, final String word) { 388 reloadDictionaryIfRequired(); 389 asyncExecuteTaskWithWriteLock(new Runnable() { 390 @Override 391 public void run() { 392 if (mBinaryDictionary == null) { 393 return; 394 } 395 runGCIfRequiredLocked(true /* mindsBlockByGC */); 396 if (!mBinaryDictionary.removeNgramEntry(prevWordsInfo, word)) { 397 if (DEBUG) { 398 Log.i(TAG, "Cannot remove n-gram entry."); 399 Log.i(TAG, " PrevWordsInfo: " + prevWordsInfo + ", word: " + word); 400 } 401 } 402 } 403 }); 404 } 405 406 public interface AddMultipleDictionaryEntriesCallback { onFinished()407 public void onFinished(); 408 } 409 410 /** 411 * Dynamically add multiple entries to the dictionary. 412 */ addMultipleDictionaryEntriesDynamically( final ArrayList<LanguageModelParam> languageModelParams, final AddMultipleDictionaryEntriesCallback callback)413 public void addMultipleDictionaryEntriesDynamically( 414 final ArrayList<LanguageModelParam> languageModelParams, 415 final AddMultipleDictionaryEntriesCallback callback) { 416 reloadDictionaryIfRequired(); 417 asyncExecuteTaskWithWriteLock(new Runnable() { 418 @Override 419 public void run() { 420 try { 421 if (mBinaryDictionary == null) { 422 return; 423 } 424 mBinaryDictionary.addMultipleDictionaryEntries( 425 languageModelParams.toArray( 426 new LanguageModelParam[languageModelParams.size()])); 427 } finally { 428 if (callback != null) { 429 callback.onFinished(); 430 } 431 } 432 } 433 }); 434 } 435 436 @Override getSuggestions(final WordComposer composer, final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId, final float[] inOutLanguageWeight)437 public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, 438 final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, 439 final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId, 440 final float[] inOutLanguageWeight) { 441 reloadDictionaryIfRequired(); 442 boolean lockAcquired = false; 443 try { 444 lockAcquired = mLock.readLock().tryLock( 445 TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS, TimeUnit.MILLISECONDS); 446 if (lockAcquired) { 447 if (mBinaryDictionary == null) { 448 return null; 449 } 450 final ArrayList<SuggestedWordInfo> suggestions = 451 mBinaryDictionary.getSuggestions(composer, prevWordsInfo, proximityInfo, 452 settingsValuesForSuggestion, sessionId, inOutLanguageWeight); 453 if (mBinaryDictionary.isCorrupted()) { 454 Log.i(TAG, "Dictionary (" + mDictName +") is corrupted. " 455 + "Remove and regenerate it."); 456 removeBinaryDictionary(); 457 } 458 return suggestions; 459 } 460 } catch (final InterruptedException e) { 461 Log.e(TAG, "Interrupted tryLock() in getSuggestionsWithSessionId().", e); 462 } finally { 463 if (lockAcquired) { 464 mLock.readLock().unlock(); 465 } 466 } 467 return null; 468 } 469 470 @Override isInDictionary(final String word)471 public boolean isInDictionary(final String word) { 472 reloadDictionaryIfRequired(); 473 boolean lockAcquired = false; 474 try { 475 lockAcquired = mLock.readLock().tryLock( 476 TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS, TimeUnit.MILLISECONDS); 477 if (lockAcquired) { 478 if (mBinaryDictionary == null) { 479 return false; 480 } 481 return isInDictionaryLocked(word); 482 } 483 } catch (final InterruptedException e) { 484 Log.e(TAG, "Interrupted tryLock() in isInDictionary().", e); 485 } finally { 486 if (lockAcquired) { 487 mLock.readLock().unlock(); 488 } 489 } 490 return false; 491 } 492 isInDictionaryLocked(final String word)493 protected boolean isInDictionaryLocked(final String word) { 494 if (mBinaryDictionary == null) return false; 495 return mBinaryDictionary.isInDictionary(word); 496 } 497 498 @Override getMaxFrequencyOfExactMatches(final String word)499 public int getMaxFrequencyOfExactMatches(final String word) { 500 reloadDictionaryIfRequired(); 501 boolean lockAcquired = false; 502 try { 503 lockAcquired = mLock.readLock().tryLock( 504 TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS, TimeUnit.MILLISECONDS); 505 if (lockAcquired) { 506 if (mBinaryDictionary == null) { 507 return NOT_A_PROBABILITY; 508 } 509 return mBinaryDictionary.getMaxFrequencyOfExactMatches(word); 510 } 511 } catch (final InterruptedException e) { 512 Log.e(TAG, "Interrupted tryLock() in getMaxFrequencyOfExactMatches().", e); 513 } finally { 514 if (lockAcquired) { 515 mLock.readLock().unlock(); 516 } 517 } 518 return NOT_A_PROBABILITY; 519 } 520 521 isValidNgramLocked(final PrevWordsInfo prevWordsInfo, final String word)522 protected boolean isValidNgramLocked(final PrevWordsInfo prevWordsInfo, final String word) { 523 if (mBinaryDictionary == null) return false; 524 return mBinaryDictionary.isValidNgram(prevWordsInfo, word); 525 } 526 527 /** 528 * Loads the current binary dictionary from internal storage. Assumes the dictionary file 529 * exists. 530 */ loadBinaryDictionaryLocked()531 private void loadBinaryDictionaryLocked() { 532 if (DBG_STRESS_TEST) { 533 // Test if this class does not cause problems when it takes long time to load binary 534 // dictionary. 535 try { 536 Log.w(TAG, "Start stress in loading: " + mDictName); 537 Thread.sleep(15000); 538 Log.w(TAG, "End stress in loading"); 539 } catch (InterruptedException e) { 540 } 541 } 542 final BinaryDictionary oldBinaryDictionary = mBinaryDictionary; 543 openBinaryDictionaryLocked(); 544 if (oldBinaryDictionary != null) { 545 oldBinaryDictionary.close(); 546 } 547 if (mBinaryDictionary.isValidDictionary() 548 && needsToMigrateDictionary(mBinaryDictionary.getFormatVersion())) { 549 if (!mBinaryDictionary.migrateTo(DICTIONARY_FORMAT_VERSION)) { 550 Log.e(TAG, "Dictionary migration failed: " + mDictName); 551 removeBinaryDictionaryLocked(); 552 } 553 } 554 } 555 556 /** 557 * Create a new binary dictionary and load initial contents. 558 */ createNewDictionaryLocked()559 private void createNewDictionaryLocked() { 560 removeBinaryDictionaryLocked(); 561 createOnMemoryBinaryDictionaryLocked(); 562 loadInitialContentsLocked(); 563 // Run GC and flush to file when initial contents have been loaded. 564 mBinaryDictionary.flushWithGCIfHasUpdated(); 565 } 566 567 /** 568 * Marks that the dictionary needs to be recreated. 569 * 570 */ setNeedsToRecreate()571 protected void setNeedsToRecreate() { 572 mNeedsToRecreate = true; 573 } 574 575 /** 576 * Load the current binary dictionary from internal storage. If the dictionary file doesn't 577 * exists or needs to be regenerated, the new dictionary file will be asynchronously generated. 578 * However, the dictionary itself is accessible even before the new dictionary file is actually 579 * generated. It may return a null result for getSuggestions() in that case by design. 580 */ reloadDictionaryIfRequired()581 public final void reloadDictionaryIfRequired() { 582 if (!isReloadRequired()) return; 583 asyncReloadDictionary(); 584 } 585 586 /** 587 * Returns whether a dictionary reload is required. 588 */ isReloadRequired()589 private boolean isReloadRequired() { 590 return mBinaryDictionary == null || mNeedsToRecreate; 591 } 592 593 /** 594 * Reloads the dictionary. Access is controlled on a per dictionary file basis. 595 */ asyncReloadDictionary()596 private final void asyncReloadDictionary() { 597 if (mIsReloading.compareAndSet(false, true)) { 598 asyncExecuteTaskWithWriteLock(new Runnable() { 599 @Override 600 public void run() { 601 try { 602 if (!mDictFile.exists() || mNeedsToRecreate) { 603 // If the dictionary file does not exist or contents have been updated, 604 // generate a new one. 605 createNewDictionaryLocked(); 606 } else if (mBinaryDictionary == null) { 607 // Otherwise, load the existing dictionary. 608 loadBinaryDictionaryLocked(); 609 if (mBinaryDictionary != null && !(isValidDictionaryLocked() 610 // TODO: remove the check below 611 && matchesExpectedBinaryDictFormatVersionForThisType( 612 mBinaryDictionary.getFormatVersion()))) { 613 // Binary dictionary or its format version is not valid. Regenerate 614 // the dictionary file. createNewDictionaryLocked will remove the 615 // existing files if appropriate. 616 createNewDictionaryLocked(); 617 } 618 } 619 mNeedsToRecreate = false; 620 } finally { 621 mIsReloading.set(false); 622 } 623 } 624 }); 625 } 626 } 627 628 /** 629 * Flush binary dictionary to dictionary file. 630 */ asyncFlushBinaryDictionary()631 public void asyncFlushBinaryDictionary() { 632 asyncExecuteTaskWithWriteLock(new Runnable() { 633 @Override 634 public void run() { 635 if (mBinaryDictionary == null) { 636 return; 637 } 638 if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) { 639 mBinaryDictionary.flushWithGC(); 640 } else { 641 mBinaryDictionary.flush(); 642 } 643 } 644 }); 645 } 646 647 @UsedForTesting waitAllTasksForTests()648 public void waitAllTasksForTests() { 649 final CountDownLatch countDownLatch = new CountDownLatch(1); 650 ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { 651 @Override 652 public void run() { 653 countDownLatch.countDown(); 654 } 655 }); 656 try { 657 countDownLatch.await(); 658 } catch (InterruptedException e) { 659 Log.e(TAG, "Interrupted while waiting for finishing dictionary operations.", e); 660 } 661 } 662 663 @UsedForTesting clearAndFlushDictionaryWithAdditionalAttributes( final Map<String, String> attributeMap)664 public void clearAndFlushDictionaryWithAdditionalAttributes( 665 final Map<String, String> attributeMap) { 666 mAdditionalAttributeMap = attributeMap; 667 clear(); 668 } 669 dumpAllWordsForDebug()670 public void dumpAllWordsForDebug() { 671 reloadDictionaryIfRequired(); 672 asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() { 673 @Override 674 public void run() { 675 Log.d(TAG, "Dump dictionary: " + mDictName); 676 try { 677 final DictionaryHeader header = mBinaryDictionary.getHeader(); 678 Log.d(TAG, "Format version: " + mBinaryDictionary.getFormatVersion()); 679 Log.d(TAG, CombinedFormatUtils.formatAttributeMap( 680 header.mDictionaryOptions.mAttributes)); 681 } catch (final UnsupportedFormatException e) { 682 Log.d(TAG, "Cannot fetch header information.", e); 683 } 684 int token = 0; 685 do { 686 final BinaryDictionary.GetNextWordPropertyResult result = 687 mBinaryDictionary.getNextWordProperty(token); 688 final WordProperty wordProperty = result.mWordProperty; 689 if (wordProperty == null) { 690 Log.d(TAG, " dictionary is empty."); 691 break; 692 } 693 Log.d(TAG, wordProperty.toString()); 694 token = result.mNextToken; 695 } while (token != 0); 696 } 697 }); 698 } 699 } 700