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