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