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