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 */ 111 protected abstract void loadInitialContentsLocked(); 112 113 private boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) { 114 return formatVersion == FormatSpec.VERSION4; 115 } 116 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 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 */ 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 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 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 162 private void asyncExecuteTaskWithWriteLock(final Runnable task) { 163 asyncExecuteTaskWithLock(mLock.writeLock(), task); 164 } 165 166 private void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) { 167 asyncPreCheckAndExecuteTaskWithLock(lock, null /* preCheckTask */, task); 168 } 169 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. 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 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 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 234 private void removeBinaryDictionary() { 235 asyncExecuteTaskWithWriteLock(new Runnable() { 236 @Override 237 public void run() { 238 removeBinaryDictionaryLocked(); 239 } 240 }); 241 } 242 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 253 private void openBinaryDictionaryLocked() { 254 mBinaryDictionary = new BinaryDictionary( 255 mDictFile.getAbsolutePath(), 0 /* offset */, mDictFile.length(), 256 true /* useFullEditDistance */, mLocale, mDictType, true /* isUpdatable */); 257 } 258 259 private void createOnMemoryBinaryDictionaryLocked() { 260 mBinaryDictionary = new BinaryDictionary( 261 mDictFile.getAbsolutePath(), true /* useFullEditDistance */, mLocale, mDictType, 262 DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); 263 } 264 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 */ 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 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 */ 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 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 */ 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 */ 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 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 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 { 407 public void onFinished(); 408 } 409 410 /** 411 * Dynamically add multiple entries to the dictionary. 412 */ 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 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 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 493 protected boolean isInDictionaryLocked(final String word) { 494 if (mBinaryDictionary == null) return false; 495 return mBinaryDictionary.isInDictionary(word); 496 } 497 498 @Override 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 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 */ 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 */ 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 */ 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 */ 581 public final void reloadDictionaryIfRequired() { 582 if (!isReloadRequired()) return; 583 asyncReloadDictionary(); 584 } 585 586 /** 587 * Returns whether a dictionary reload is required. 588 */ 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 */ 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 */ 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 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 664 public void clearAndFlushDictionaryWithAdditionalAttributes( 665 final Map<String, String> attributeMap) { 666 mAdditionalAttributeMap = attributeMap; 667 clear(); 668 } 669 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