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