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