1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15 package com.android.inputmethod.latin; 16 17 import android.content.Context; 18 import android.os.SystemClock; 19 import android.util.Log; 20 21 import com.android.inputmethod.keyboard.ProximityInfo; 22 import com.android.inputmethod.latin.makedict.BinaryDictInputOutput; 23 import com.android.inputmethod.latin.makedict.FusionDictionary; 24 import com.android.inputmethod.latin.makedict.FusionDictionary.Node; 25 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; 26 import com.android.inputmethod.latin.makedict.UnsupportedFormatException; 27 28 import java.io.File; 29 import java.io.FileOutputStream; 30 import java.io.IOException; 31 import java.util.ArrayList; 32 import java.util.HashMap; 33 import java.util.concurrent.locks.ReentrantLock; 34 35 /** 36 * Abstract base class for an expandable dictionary that can be created and updated dynamically 37 * during runtime. When updated it automatically generates a new binary dictionary to handle future 38 * queries in native code. This binary dictionary is written to internal storage, and potentially 39 * shared across multiple ExpandableBinaryDictionary instances. Updates to each dictionary filename 40 * are controlled across multiple instances to ensure that only one instance can update the same 41 * dictionary at the same time. 42 */ 43 abstract public class ExpandableBinaryDictionary extends Dictionary { 44 45 /** Used for Log actions from this class */ 46 private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName(); 47 48 /** Whether to print debug output to log */ 49 private static boolean DEBUG = false; 50 51 /** 52 * The maximum length of a word in this dictionary. This is the same value as the binary 53 * dictionary. 54 */ 55 protected static final int MAX_WORD_LENGTH = BinaryDictionary.MAX_WORD_LENGTH; 56 57 /** 58 * A static map of locks, each of which controls access to a single binary dictionary file. They 59 * ensure that only one instance can update the same dictionary at the same time. The key for 60 * this map is the filename and the value is the shared dictionary controller associated with 61 * that filename. 62 */ 63 private static final HashMap<String, DictionaryController> sSharedDictionaryControllers = 64 new HashMap<String, DictionaryController>(); 65 66 /** The application context. */ 67 protected final Context mContext; 68 69 /** 70 * The binary dictionary generated dynamically from the fusion dictionary. This is used to 71 * answer unigram and bigram queries. 72 */ 73 private BinaryDictionary mBinaryDictionary; 74 75 /** The expandable fusion dictionary used to generate the binary dictionary. */ 76 private FusionDictionary mFusionDictionary; 77 78 /** The dictionary type id. */ 79 public final int mDicTypeId; 80 81 /** 82 * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple 83 * dictionary instances with the same filename is supported, with access controlled by 84 * DictionaryController. 85 */ 86 private final String mFilename; 87 88 /** Controls access to the shared binary dictionary file across multiple instances. */ 89 private final DictionaryController mSharedDictionaryController; 90 91 /** Controls access to the local binary dictionary for this instance. */ 92 private final DictionaryController mLocalDictionaryController = new DictionaryController(); 93 94 /** 95 * Abstract method for loading the unigrams and bigrams of a given dictionary in a background 96 * thread. 97 */ 98 protected abstract void loadDictionaryAsync(); 99 100 /** 101 * Indicates that the source dictionary content has changed and a rebuild of the binary file is 102 * required. If it returns false, the next reload will only read the current binary dictionary 103 * from file. Note that the shared binary dictionary is locked when this is called. 104 */ 105 protected abstract boolean hasContentChanged(); 106 107 /** 108 * Gets the shared dictionary controller for the given filename. 109 */ 110 private static synchronized DictionaryController getSharedDictionaryController( 111 String filename) { 112 DictionaryController controller = sSharedDictionaryControllers.get(filename); 113 if (controller == null) { 114 controller = new DictionaryController(); 115 sSharedDictionaryControllers.put(filename, controller); 116 } 117 return controller; 118 } 119 120 /** 121 * Creates a new expandable binary dictionary. 122 * 123 * @param context The application context of the parent. 124 * @param filename The filename for this binary dictionary. Multiple dictionaries with the same 125 * filename is supported. 126 * @param dictType The type of this dictionary. 127 */ 128 public ExpandableBinaryDictionary( 129 final Context context, final String filename, final int dictType) { 130 mDicTypeId = dictType; 131 mFilename = filename; 132 mContext = context; 133 mBinaryDictionary = null; 134 mSharedDictionaryController = getSharedDictionaryController(filename); 135 clearFusionDictionary(); 136 } 137 138 protected static String getFilenameWithLocale(final String name, final String localeStr) { 139 return name + "." + localeStr + ".dict"; 140 } 141 142 /** 143 * Closes and cleans up the binary dictionary. 144 */ 145 @Override 146 public void close() { 147 // Ensure that no other threads are accessing the local binary dictionary. 148 mLocalDictionaryController.lock(); 149 try { 150 if (mBinaryDictionary != null) { 151 mBinaryDictionary.close(); 152 mBinaryDictionary = null; 153 } 154 } finally { 155 mLocalDictionaryController.unlock(); 156 } 157 } 158 159 /** 160 * Clears the fusion dictionary on the Java side. Note: Does not modify the binary dictionary on 161 * the native side. 162 */ 163 public void clearFusionDictionary() { 164 mFusionDictionary = new FusionDictionary(new Node(), 165 new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false, 166 false)); 167 } 168 169 /** 170 * Adds a word unigram to the fusion dictionary. Call updateBinaryDictionary when all changes 171 * are done to update the binary dictionary. 172 */ 173 // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries, 174 // considering performance regression. 175 protected void addWord(final String word, final String shortcutTarget, final int frequency) { 176 if (shortcutTarget == null) { 177 mFusionDictionary.add(word, frequency, null); 178 } else { 179 // TODO: Do this in the subclass, with this class taking an arraylist. 180 final ArrayList<WeightedString> shortcutTargets = new ArrayList<WeightedString>(); 181 shortcutTargets.add(new WeightedString(shortcutTarget, frequency)); 182 mFusionDictionary.add(word, frequency, shortcutTargets); 183 } 184 } 185 186 /** 187 * Sets a word bigram in the fusion dictionary. Call updateBinaryDictionary when all changes are 188 * done to update the binary dictionary. 189 */ 190 // TODO: Create "cache dictionary" to cache fresh bigrams for frequently updated dictionaries, 191 // considering performance regression. 192 protected void setBigram(final String prevWord, final String word, final int frequency) { 193 mFusionDictionary.setBigram(prevWord, word, frequency); 194 } 195 196 @Override 197 public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams, 198 final WordCallback callback, final ProximityInfo proximityInfo) { 199 asyncReloadDictionaryIfRequired(); 200 getWordsInner(codes, prevWordForBigrams, callback, proximityInfo); 201 } 202 203 protected final void getWordsInner(final WordComposer codes, 204 final CharSequence prevWordForBigrams, final WordCallback callback, 205 final ProximityInfo proximityInfo) { 206 // Ensure that there are no concurrent calls to getWords. If there are, do nothing and 207 // return. 208 if (mLocalDictionaryController.tryLock()) { 209 try { 210 if (mBinaryDictionary != null) { 211 mBinaryDictionary.getWords(codes, prevWordForBigrams, callback, proximityInfo); 212 } 213 } finally { 214 mLocalDictionaryController.unlock(); 215 } 216 } 217 } 218 219 @Override 220 public void getBigrams(final WordComposer codes, final CharSequence previousWord, 221 final WordCallback callback) { 222 asyncReloadDictionaryIfRequired(); 223 getBigramsInner(codes, previousWord, callback); 224 } 225 226 protected void getBigramsInner(final WordComposer codes, final CharSequence previousWord, 227 final WordCallback callback) { 228 if (mLocalDictionaryController.tryLock()) { 229 try { 230 if (mBinaryDictionary != null) { 231 mBinaryDictionary.getBigrams(codes, previousWord, callback); 232 } 233 } finally { 234 mLocalDictionaryController.unlock(); 235 } 236 } 237 } 238 239 @Override 240 public boolean isValidWord(final CharSequence word) { 241 asyncReloadDictionaryIfRequired(); 242 return isValidWordInner(word); 243 } 244 245 protected boolean isValidWordInner(final CharSequence word) { 246 if (mLocalDictionaryController.tryLock()) { 247 try { 248 return isValidWordLocked(word); 249 } finally { 250 mLocalDictionaryController.unlock(); 251 } 252 } 253 return false; 254 } 255 256 protected boolean isValidWordLocked(final CharSequence word) { 257 if (mBinaryDictionary == null) return false; 258 return mBinaryDictionary.isValidWord(word); 259 } 260 261 protected boolean isValidBigram(final CharSequence word1, final CharSequence word2) { 262 if (mBinaryDictionary == null) return false; 263 return mBinaryDictionary.isValidBigram(word1, word2); 264 } 265 266 protected boolean isValidBigramInner(final CharSequence word1, final CharSequence word2) { 267 if (mLocalDictionaryController.tryLock()) { 268 try { 269 return isValidBigramLocked(word1, word2); 270 } finally { 271 mLocalDictionaryController.unlock(); 272 } 273 } 274 return false; 275 } 276 277 protected boolean isValidBigramLocked(final CharSequence word1, final CharSequence word2) { 278 if (mBinaryDictionary == null) return false; 279 return mBinaryDictionary.isValidBigram(word1, word2); 280 } 281 282 /** 283 * Load the current binary dictionary from internal storage in a background thread. If no binary 284 * dictionary exists, this method will generate one. 285 */ 286 protected void loadDictionary() { 287 mLocalDictionaryController.mLastUpdateRequestTime = SystemClock.uptimeMillis(); 288 asyncReloadDictionaryIfRequired(); 289 } 290 291 /** 292 * Loads the current binary dictionary from internal storage. Assumes the dictionary file 293 * exists. 294 */ 295 protected void loadBinaryDictionary() { 296 if (DEBUG) { 297 Log.d(TAG, "Loading binary dictionary: " + mFilename + " request=" 298 + mSharedDictionaryController.mLastUpdateRequestTime + " update=" 299 + mSharedDictionaryController.mLastUpdateTime); 300 } 301 302 final File file = new File(mContext.getFilesDir(), mFilename); 303 final String filename = file.getAbsolutePath(); 304 final long length = file.length(); 305 306 // Build the new binary dictionary 307 final BinaryDictionary newBinaryDictionary = 308 new BinaryDictionary(mContext, filename, 0, length, true /* useFullEditDistance */, 309 null); 310 311 if (mBinaryDictionary != null) { 312 // Ensure all threads accessing the current dictionary have finished before swapping in 313 // the new one. 314 final BinaryDictionary oldBinaryDictionary = mBinaryDictionary; 315 mLocalDictionaryController.lock(); 316 mBinaryDictionary = newBinaryDictionary; 317 mLocalDictionaryController.unlock(); 318 oldBinaryDictionary.close(); 319 } else { 320 mBinaryDictionary = newBinaryDictionary; 321 } 322 } 323 324 /** 325 * Generates and writes a new binary dictionary based on the contents of the fusion dictionary. 326 */ 327 private void generateBinaryDictionary() { 328 if (DEBUG) { 329 Log.d(TAG, "Generating binary dictionary: " + mFilename + " request=" 330 + mSharedDictionaryController.mLastUpdateRequestTime + " update=" 331 + mSharedDictionaryController.mLastUpdateTime); 332 } 333 334 loadDictionaryAsync(); 335 336 final String tempFileName = mFilename + ".temp"; 337 final File file = new File(mContext.getFilesDir(), mFilename); 338 final File tempFile = new File(mContext.getFilesDir(), tempFileName); 339 FileOutputStream out = null; 340 try { 341 out = new FileOutputStream(tempFile); 342 BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, 1); 343 out.flush(); 344 out.close(); 345 tempFile.renameTo(file); 346 clearFusionDictionary(); 347 } catch (IOException e) { 348 Log.e(TAG, "IO exception while writing file: " + e); 349 } catch (UnsupportedFormatException e) { 350 Log.e(TAG, "Unsupported format: " + e); 351 } finally { 352 if (out != null) { 353 try { 354 out.close(); 355 } catch (IOException e) { 356 // ignore 357 } 358 } 359 } 360 } 361 362 /** 363 * Marks that the dictionary is out of date and requires a reload. 364 * 365 * @param requiresRebuild Indicates that the source dictionary content has changed and a rebuild 366 * of the binary file is required. If not true, the next reload process will only read 367 * the current binary dictionary from file. 368 */ 369 protected void setRequiresReload(final boolean requiresRebuild) { 370 final long time = SystemClock.uptimeMillis(); 371 mLocalDictionaryController.mLastUpdateRequestTime = time; 372 mSharedDictionaryController.mLastUpdateRequestTime = time; 373 if (DEBUG) { 374 Log.d(TAG, "Reload request: " + mFilename + ": request=" + time + " update=" 375 + mSharedDictionaryController.mLastUpdateTime); 376 } 377 } 378 379 /** 380 * Reloads the dictionary if required. Reload will occur asynchronously in a separate thread. 381 */ 382 void asyncReloadDictionaryIfRequired() { 383 if (!isReloadRequired()) return; 384 if (DEBUG) { 385 Log.d(TAG, "Starting AsyncReloadDictionaryTask: " + mFilename); 386 } 387 new AsyncReloadDictionaryTask().start(); 388 } 389 390 /** 391 * Reloads the dictionary if required. 392 */ 393 protected final void syncReloadDictionaryIfRequired() { 394 if (!isReloadRequired()) return; 395 syncReloadDictionaryInternal(); 396 } 397 398 /** 399 * Returns whether a dictionary reload is required. 400 */ 401 private boolean isReloadRequired() { 402 return mBinaryDictionary == null || mLocalDictionaryController.isOutOfDate(); 403 } 404 405 /** 406 * Reloads the dictionary. Access is controlled on a per dictionary file basis and supports 407 * concurrent calls from multiple instances that share the same dictionary file. 408 */ 409 private final void syncReloadDictionaryInternal() { 410 // Ensure that only one thread attempts to read or write to the shared binary dictionary 411 // file at the same time. 412 mSharedDictionaryController.lock(); 413 try { 414 final long time = SystemClock.uptimeMillis(); 415 final boolean dictionaryFileExists = dictionaryFileExists(); 416 if (mSharedDictionaryController.isOutOfDate() || !dictionaryFileExists) { 417 // If the shared dictionary file does not exist or is out of date, the first 418 // instance that acquires the lock will generate a new one. 419 if (hasContentChanged() || !dictionaryFileExists) { 420 // If the source content has changed or the dictionary does not exist, rebuild 421 // the binary dictionary. Empty dictionaries are supported (in the case where 422 // loadDictionaryAsync() adds nothing) in order to provide a uniform framework. 423 mSharedDictionaryController.mLastUpdateTime = time; 424 generateBinaryDictionary(); 425 loadBinaryDictionary(); 426 } else { 427 // If not, the reload request was unnecessary so revert LastUpdateRequestTime 428 // to LastUpdateTime. 429 mSharedDictionaryController.mLastUpdateRequestTime = 430 mSharedDictionaryController.mLastUpdateTime; 431 } 432 } else if (mBinaryDictionary == null || mLocalDictionaryController.mLastUpdateTime 433 < mSharedDictionaryController.mLastUpdateTime) { 434 // Otherwise, if the local dictionary is older than the shared dictionary, load the 435 // shared dictionary. 436 loadBinaryDictionary(); 437 } 438 mLocalDictionaryController.mLastUpdateTime = time; 439 } finally { 440 mSharedDictionaryController.unlock(); 441 } 442 } 443 444 // TODO: cache the file's existence so that we avoid doing a disk access each time. 445 private boolean dictionaryFileExists() { 446 final File file = new File(mContext.getFilesDir(), mFilename); 447 return file.exists(); 448 } 449 450 /** 451 * Thread class for asynchronously reloading and rewriting the binary dictionary. 452 */ 453 private class AsyncReloadDictionaryTask extends Thread { 454 @Override 455 public void run() { 456 syncReloadDictionaryInternal(); 457 } 458 } 459 460 /** 461 * Lock for controlling access to a given binary dictionary and for tracking whether the 462 * dictionary is out of date. Can be shared across multiple dictionary instances that access the 463 * same filename. 464 */ 465 private static class DictionaryController extends ReentrantLock { 466 private volatile long mLastUpdateTime = 0; 467 private volatile long mLastUpdateRequestTime = 0; 468 469 private boolean isOutOfDate() { 470 return (mLastUpdateRequestTime > mLastUpdateTime); 471 } 472 } 473 } 474