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 */ 115 protected abstract void loadInitialContentsLocked(); 116 117 static boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) { 118 return formatVersion == FormatSpec.VERSION4; 119 } 120 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 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 */ 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 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 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 165 private void asyncExecuteTaskWithWriteLock(final Runnable task) { 166 asyncExecuteTaskWithLock(mLock.writeLock(), task); 167 } 168 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 184 BinaryDictionary getBinaryDictionary() { 185 return mBinaryDictionary; 186 } 187 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 199 public void close() { 200 asyncExecuteTaskWithWriteLock(new Runnable() { 201 @Override 202 public void run() { 203 closeBinaryDictionary(); 204 } 205 }); 206 } 207 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 220 private void removeBinaryDictionary() { 221 asyncExecuteTaskWithWriteLock(new Runnable() { 222 @Override 223 public void run() { 224 removeBinaryDictionaryLocked(); 225 } 226 }); 227 } 228 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 236 private void openBinaryDictionaryLocked() { 237 mBinaryDictionary = new BinaryDictionary( 238 mDictFile.getAbsolutePath(), 0 /* offset */, mDictFile.length(), 239 true /* useFullEditDistance */, mLocale, mDictType, true /* isUpdatable */); 240 } 241 242 void createOnMemoryBinaryDictionaryLocked() { 243 mBinaryDictionary = new BinaryDictionary( 244 mDictFile.getAbsolutePath(), true /* useFullEditDistance */, mLocale, mDictType, 245 DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); 246 } 247 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 */ 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 273 protected void runGCIfRequiredLocked(final boolean mindsBlockByGC) { 274 if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) { 275 mBinaryDictionary.flushWithGC(); 276 } 277 } 278 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 */ 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 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 */ 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 */ 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 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 */ 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 { 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 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 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 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 486 protected boolean isInDictionaryLocked(final String word) { 487 if (mBinaryDictionary == null) return false; 488 return mBinaryDictionary.isInDictionary(word); 489 } 490 491 @Override 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 */ 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 */ 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 */ 560 protected void setNeedsToRecreate() { 561 mNeedsToRecreate = true; 562 } 563 564 void clearNeedsToRecreate() { 565 mNeedsToRecreate = false; 566 } 567 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 */ 578 public final void reloadDictionaryIfRequired() { 579 if (!isReloadRequired()) return; 580 asyncReloadDictionary(); 581 } 582 583 /** 584 * Returns whether a dictionary reload is required. 585 */ 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 */ 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 */ 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 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 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 681 public void clearAndFlushDictionaryWithAdditionalAttributes( 682 final Map<String, String> attributeMap) { 683 mAdditionalAttributeMap = attributeMap; 684 clear(); 685 } 686 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 */ 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