ExpandableBinaryDictionary.java revision ffcbbaf12788a9fc9398607a548e552d7d2bf05e
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.os.SystemClock; 21import android.util.Log; 22 23import com.android.inputmethod.keyboard.ProximityInfo; 24import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 25import com.android.inputmethod.latin.utils.CollectionUtils; 26 27import java.io.File; 28import java.util.ArrayList; 29import java.util.HashMap; 30import java.util.concurrent.locks.ReentrantReadWriteLock; 31 32/** 33 * Abstract base class for an expandable dictionary that can be created and updated dynamically 34 * during runtime. When updated it automatically generates a new binary dictionary to handle future 35 * queries in native code. This binary dictionary is written to internal storage, and potentially 36 * shared across multiple ExpandableBinaryDictionary instances. Updates to each dictionary filename 37 * are controlled across multiple instances to ensure that only one instance can update the same 38 * dictionary at the same time. 39 */ 40abstract public class ExpandableBinaryDictionary extends Dictionary { 41 42 /** Used for Log actions from this class */ 43 private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName(); 44 45 /** Whether to print debug output to log */ 46 private static boolean DEBUG = false; 47 48 /** 49 * The maximum length of a word in this dictionary. 50 */ 51 protected static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; 52 53 /** 54 * A static map of locks, each of which controls access to a single binary dictionary file. They 55 * ensure that only one instance can update the same dictionary at the same time. The key for 56 * this map is the filename and the value is the shared dictionary controller associated with 57 * that filename. 58 */ 59 private static final HashMap<String, DictionaryController> sSharedDictionaryControllers = 60 CollectionUtils.newHashMap(); 61 62 /** The application context. */ 63 protected final Context mContext; 64 65 /** 66 * The binary dictionary generated dynamically from the fusion dictionary. This is used to 67 * answer unigram and bigram queries. 68 */ 69 private BinaryDictionary mBinaryDictionary; 70 71 /** The in-memory dictionary used to generate the binary dictionary. */ 72 private AbstractDictionaryWriter mDictionaryWriter; 73 74 /** 75 * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple 76 * dictionary instances with the same filename is supported, with access controlled by 77 * DictionaryController. 78 */ 79 private final String mFilename; 80 81 /** Controls access to the shared binary dictionary file across multiple instances. */ 82 private final DictionaryController mSharedDictionaryController; 83 84 /** Controls access to the local binary dictionary for this instance. */ 85 private final DictionaryController mLocalDictionaryController = new DictionaryController(); 86 87 /** 88 * Abstract method for loading the unigrams and bigrams of a given dictionary in a background 89 * thread. 90 */ 91 protected abstract void loadDictionaryAsync(); 92 93 /** 94 * Indicates that the source dictionary content has changed and a rebuild of the binary file is 95 * required. If it returns false, the next reload will only read the current binary dictionary 96 * from file. Note that the shared binary dictionary is locked when this is called. 97 */ 98 protected abstract boolean hasContentChanged(); 99 100 /** 101 * Gets the shared dictionary controller for the given filename. 102 */ 103 private static synchronized DictionaryController getSharedDictionaryController( 104 String filename) { 105 DictionaryController controller = sSharedDictionaryControllers.get(filename); 106 if (controller == null) { 107 controller = new DictionaryController(); 108 sSharedDictionaryControllers.put(filename, controller); 109 } 110 return controller; 111 } 112 113 /** 114 * Creates a new expandable binary dictionary. 115 * 116 * @param context The application context of the parent. 117 * @param filename The filename for this binary dictionary. Multiple dictionaries with the same 118 * filename is supported. 119 * @param dictType the dictionary type, as a human-readable string 120 */ 121 public ExpandableBinaryDictionary( 122 final Context context, final String filename, final String dictType) { 123 super(dictType); 124 mFilename = filename; 125 mContext = context; 126 mBinaryDictionary = null; 127 mSharedDictionaryController = getSharedDictionaryController(filename); 128 mDictionaryWriter = new DictionaryWriter(context, dictType); 129 } 130 131 protected static String getFilenameWithLocale(final String name, final String localeStr) { 132 return name + "." + localeStr + ".dict"; 133 } 134 135 /** 136 * Closes and cleans up the binary dictionary. 137 */ 138 @Override 139 public void close() { 140 // Ensure that no other threads are accessing the local binary dictionary. 141 mLocalDictionaryController.writeLock().lock(); 142 try { 143 if (mBinaryDictionary != null) { 144 mBinaryDictionary.close(); 145 mBinaryDictionary = null; 146 } 147 mDictionaryWriter.close(); 148 } finally { 149 mLocalDictionaryController.writeLock().unlock(); 150 } 151 } 152 153 /** 154 * Adds a word unigram to the dictionary. Used for loading a dictionary. 155 */ 156 protected void addWord(final String word, final String shortcutTarget, 157 final int frequency, final boolean isNotAWord) { 158 mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord); 159 } 160 161 /** 162 * Sets a word bigram in the dictionary. Used for loading a dictionary. 163 */ 164 protected void setBigram(final String prevWord, final String word, final int frequency) { 165 mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */); 166 } 167 168 /** 169 * Dynamically adds a word unigram to the dictionary. 170 */ 171 protected void addWordDynamically(final String word, final String shortcutTarget, 172 final int frequency, final boolean isNotAWord) { 173 mLocalDictionaryController.writeLock().lock(); 174 try { 175 mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord); 176 } finally { 177 mLocalDictionaryController.writeLock().unlock(); 178 } 179 } 180 181 /** 182 * Dynamically sets a word bigram in the dictionary. 183 */ 184 protected void setBigramDynamically(final String prevWord, final String word, 185 final int frequency) { 186 mLocalDictionaryController.writeLock().lock(); 187 try { 188 mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */); 189 } finally { 190 mLocalDictionaryController.writeLock().unlock(); 191 } 192 } 193 194 @Override 195 public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, 196 final String prevWord, final ProximityInfo proximityInfo, 197 final boolean blockOffensiveWords) { 198 asyncReloadDictionaryIfRequired(); 199 // Write lock because getSuggestions in native updates session status. 200 if (mLocalDictionaryController.writeLock().tryLock()) { 201 try { 202 final ArrayList<SuggestedWordInfo> inMemDictSuggestion = 203 mDictionaryWriter.getSuggestions(composer, prevWord, proximityInfo, 204 blockOffensiveWords); 205 if (mBinaryDictionary != null) { 206 final ArrayList<SuggestedWordInfo> binarySuggestion = 207 mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo, 208 blockOffensiveWords); 209 if (inMemDictSuggestion == null) { 210 return binarySuggestion; 211 } else if (binarySuggestion == null) { 212 return inMemDictSuggestion; 213 } else { 214 binarySuggestion.addAll(binarySuggestion); 215 return binarySuggestion; 216 } 217 } else { 218 return inMemDictSuggestion; 219 } 220 } finally { 221 mLocalDictionaryController.writeLock().unlock(); 222 } 223 } 224 return null; 225 } 226 227 @Override 228 public boolean isValidWord(final String word) { 229 asyncReloadDictionaryIfRequired(); 230 return isValidWordInner(word); 231 } 232 233 protected boolean isValidWordInner(final String word) { 234 if (mLocalDictionaryController.readLock().tryLock()) { 235 try { 236 return isValidWordLocked(word); 237 } finally { 238 mLocalDictionaryController.readLock().unlock(); 239 } 240 } 241 return false; 242 } 243 244 protected boolean isValidWordLocked(final String word) { 245 if (mBinaryDictionary == null) return false; 246 return mBinaryDictionary.isValidWord(word); 247 } 248 249 protected boolean isValidBigramLocked(final String word1, final String word2) { 250 if (mBinaryDictionary == null) return false; 251 return mBinaryDictionary.isValidBigram(word1, word2); 252 } 253 254 /** 255 * Load the current binary dictionary from internal storage in a background thread. If no binary 256 * dictionary exists, this method will generate one. 257 */ 258 protected void loadDictionary() { 259 mLocalDictionaryController.mLastUpdateRequestTime = SystemClock.uptimeMillis(); 260 asyncReloadDictionaryIfRequired(); 261 } 262 263 /** 264 * Loads the current binary dictionary from internal storage. Assumes the dictionary file 265 * exists. 266 */ 267 private void loadBinaryDictionary() { 268 if (DEBUG) { 269 Log.d(TAG, "Loading binary dictionary: " + mFilename + " request=" 270 + mSharedDictionaryController.mLastUpdateRequestTime + " update=" 271 + mSharedDictionaryController.mLastUpdateTime); 272 } 273 274 final File file = new File(mContext.getFilesDir(), mFilename); 275 final String filename = file.getAbsolutePath(); 276 final long length = file.length(); 277 278 // Build the new binary dictionary 279 final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0, length, 280 true /* useFullEditDistance */, null, mDictType, false /* isUpdatable */); 281 282 if (mBinaryDictionary != null) { 283 // Ensure all threads accessing the current dictionary have finished before swapping in 284 // the new one. 285 final BinaryDictionary oldBinaryDictionary = mBinaryDictionary; 286 mLocalDictionaryController.writeLock().lock(); 287 try { 288 mBinaryDictionary = newBinaryDictionary; 289 } finally { 290 mLocalDictionaryController.writeLock().unlock(); 291 } 292 oldBinaryDictionary.close(); 293 } else { 294 mBinaryDictionary = newBinaryDictionary; 295 } 296 } 297 298 /** 299 * Abstract method for checking if it is required to reload the dictionary before writing 300 * a binary dictionary. 301 */ 302 abstract protected boolean needsToReloadBeforeWriting(); 303 304 /** 305 * Generates and writes a new binary dictionary based on the contents of the fusion dictionary. 306 */ 307 private void generateBinaryDictionary() { 308 if (DEBUG) { 309 Log.d(TAG, "Generating binary dictionary: " + mFilename + " request=" 310 + mSharedDictionaryController.mLastUpdateRequestTime + " update=" 311 + mSharedDictionaryController.mLastUpdateTime); 312 } 313 if (needsToReloadBeforeWriting()) { 314 mDictionaryWriter.clear(); 315 loadDictionaryAsync(); 316 } 317 mDictionaryWriter.write(mFilename); 318 } 319 320 /** 321 * Marks that the dictionary is out of date and requires a reload. 322 * 323 * @param requiresRebuild Indicates that the source dictionary content has changed and a rebuild 324 * of the binary file is required. If not true, the next reload process will only read 325 * the current binary dictionary from file. 326 */ 327 protected void setRequiresReload(final boolean requiresRebuild) { 328 final long time = SystemClock.uptimeMillis(); 329 mLocalDictionaryController.mLastUpdateRequestTime = time; 330 mSharedDictionaryController.mLastUpdateRequestTime = time; 331 if (DEBUG) { 332 Log.d(TAG, "Reload request: " + mFilename + ": request=" + time + " update=" 333 + mSharedDictionaryController.mLastUpdateTime); 334 } 335 } 336 337 /** 338 * Reloads the dictionary if required. Reload will occur asynchronously in a separate thread. 339 */ 340 void asyncReloadDictionaryIfRequired() { 341 if (!isReloadRequired()) return; 342 if (DEBUG) { 343 Log.d(TAG, "Starting AsyncReloadDictionaryTask: " + mFilename); 344 } 345 new AsyncReloadDictionaryTask().start(); 346 } 347 348 /** 349 * Reloads the dictionary if required. 350 */ 351 protected final void syncReloadDictionaryIfRequired() { 352 if (!isReloadRequired()) return; 353 syncReloadDictionaryInternal(); 354 } 355 356 /** 357 * Returns whether a dictionary reload is required. 358 */ 359 private boolean isReloadRequired() { 360 return mBinaryDictionary == null || mLocalDictionaryController.isOutOfDate(); 361 } 362 363 /** 364 * Reloads the dictionary. Access is controlled on a per dictionary file basis and supports 365 * concurrent calls from multiple instances that share the same dictionary file. 366 */ 367 private final void syncReloadDictionaryInternal() { 368 // Ensure that only one thread attempts to read or write to the shared binary dictionary 369 // file at the same time. 370 mSharedDictionaryController.writeLock().lock(); 371 try { 372 final long time = SystemClock.uptimeMillis(); 373 final boolean dictionaryFileExists = dictionaryFileExists(); 374 if (mSharedDictionaryController.isOutOfDate() || !dictionaryFileExists) { 375 // If the shared dictionary file does not exist or is out of date, the first 376 // instance that acquires the lock will generate a new one. 377 if (hasContentChanged() || !dictionaryFileExists) { 378 // If the source content has changed or the dictionary does not exist, rebuild 379 // the binary dictionary. Empty dictionaries are supported (in the case where 380 // loadDictionaryAsync() adds nothing) in order to provide a uniform framework. 381 mSharedDictionaryController.mLastUpdateTime = time; 382 generateBinaryDictionary(); 383 loadBinaryDictionary(); 384 } else { 385 // If not, the reload request was unnecessary so revert LastUpdateRequestTime 386 // to LastUpdateTime. 387 mSharedDictionaryController.mLastUpdateRequestTime = 388 mSharedDictionaryController.mLastUpdateTime; 389 } 390 } else if (mBinaryDictionary == null || mLocalDictionaryController.mLastUpdateTime 391 < mSharedDictionaryController.mLastUpdateTime) { 392 // Otherwise, if the local dictionary is older than the shared dictionary, load the 393 // shared dictionary. 394 loadBinaryDictionary(); 395 } 396 if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) { 397 // Binary dictionary is not valid. Regenerate the dictionary file. 398 mSharedDictionaryController.mLastUpdateTime = time; 399 generateBinaryDictionary(); 400 loadBinaryDictionary(); 401 } 402 mLocalDictionaryController.mLastUpdateTime = time; 403 } finally { 404 mSharedDictionaryController.writeLock().unlock(); 405 } 406 } 407 408 // TODO: cache the file's existence so that we avoid doing a disk access each time. 409 private boolean dictionaryFileExists() { 410 final File file = new File(mContext.getFilesDir(), mFilename); 411 return file.exists(); 412 } 413 414 /** 415 * Thread class for asynchronously reloading and rewriting the binary dictionary. 416 */ 417 private class AsyncReloadDictionaryTask extends Thread { 418 @Override 419 public void run() { 420 syncReloadDictionaryInternal(); 421 } 422 } 423 424 /** 425 * Lock for controlling access to a given binary dictionary and for tracking whether the 426 * dictionary is out of date. Can be shared across multiple dictionary instances that access the 427 * same filename. 428 */ 429 private static class DictionaryController extends ReentrantReadWriteLock { 430 private volatile long mLastUpdateTime = 0; 431 private volatile long mLastUpdateRequestTime = 0; 432 433 private boolean isOutOfDate() { 434 return (mLastUpdateRequestTime > mLastUpdateTime); 435 } 436 } 437} 438