/ime/latinime/src/com/googlecode/eyesfree/inputmethod/latin/AutoDictionary.java

http://eyes-free.googlecode.com/ · Java · 259 lines · 170 code · 31 blank · 58 comment · 17 complexity · bf54b64d36de9acf57df0d07b172ed51 MD5 · raw file

  1. /*
  2. * Copyright (C) 2010 Google Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * 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, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.googlecode.eyesfree.inputmethod.latin;
  17. import android.content.ContentValues;
  18. import android.content.Context;
  19. import android.database.Cursor;
  20. import android.database.sqlite.SQLiteDatabase;
  21. import android.database.sqlite.SQLiteOpenHelper;
  22. import android.database.sqlite.SQLiteQueryBuilder;
  23. import android.os.AsyncTask;
  24. import android.provider.BaseColumns;
  25. import android.util.Log;
  26. import java.util.HashMap;
  27. import java.util.Map.Entry;
  28. import java.util.Set;
  29. /**
  30. * Stores new words temporarily until they are promoted to the user dictionary
  31. * for longevity. Words in the auto dictionary are used to determine if it's ok
  32. * to accept a word that's not in the main or user dictionary. Using a new word
  33. * repeatedly will promote it to the user dictionary.
  34. */
  35. public class AutoDictionary extends ExpandableDictionary {
  36. // Weight added to a user picking a new word from the suggestion strip
  37. static final int FREQUENCY_FOR_PICKED = 3;
  38. // Weight added to a user typing a new word that doesn't get corrected (or is reverted)
  39. static final int FREQUENCY_FOR_TYPED = 1;
  40. // A word that is frequently typed and gets promoted to the user dictionary, uses this
  41. // frequency.
  42. static final int FREQUENCY_FOR_AUTO_ADD = 250;
  43. // If the user touches a typed word 2 times or more, it will become valid.
  44. private static final int VALIDITY_THRESHOLD = 2 * FREQUENCY_FOR_PICKED;
  45. // If the user touches a typed word 4 times or more, it will be added to the user dict.
  46. private static final int PROMOTION_THRESHOLD = 4 * FREQUENCY_FOR_PICKED;
  47. private LatinIME mIme;
  48. // Locale for which this auto dictionary is storing words
  49. private String mLocale;
  50. private HashMap<String,Integer> mPendingWrites = new HashMap<String,Integer>();
  51. private final Object mPendingWritesLock = new Object();
  52. private static final String DATABASE_NAME = "auto_dict.db";
  53. private static final int DATABASE_VERSION = 1;
  54. // These are the columns in the dictionary
  55. // TODO: Consume less space by using a unique id for locale instead of the whole
  56. // 2-5 character string.
  57. private static final String COLUMN_ID = BaseColumns._ID;
  58. private static final String COLUMN_WORD = "word";
  59. private static final String COLUMN_FREQUENCY = "freq";
  60. private static final String COLUMN_LOCALE = "locale";
  61. /** Sort by descending order of frequency. */
  62. public static final String DEFAULT_SORT_ORDER = COLUMN_FREQUENCY + " DESC";
  63. /** Name of the words table in the auto_dict.db */
  64. private static final String AUTODICT_TABLE_NAME = "words";
  65. private static HashMap<String, String> sDictProjectionMap;
  66. static {
  67. sDictProjectionMap = new HashMap<String, String>();
  68. sDictProjectionMap.put(COLUMN_ID, COLUMN_ID);
  69. sDictProjectionMap.put(COLUMN_WORD, COLUMN_WORD);
  70. sDictProjectionMap.put(COLUMN_FREQUENCY, COLUMN_FREQUENCY);
  71. sDictProjectionMap.put(COLUMN_LOCALE, COLUMN_LOCALE);
  72. }
  73. private static DatabaseHelper sOpenHelper = null;
  74. public AutoDictionary(Context context, LatinIME ime, String locale, int dicTypeId) {
  75. super(context, dicTypeId);
  76. mIme = ime;
  77. mLocale = locale;
  78. if (sOpenHelper == null) {
  79. sOpenHelper = new DatabaseHelper(getContext());
  80. }
  81. if (mLocale != null && mLocale.length() > 1) {
  82. loadDictionary();
  83. }
  84. }
  85. @Override
  86. public boolean isValidWord(CharSequence word) {
  87. final int frequency = getWordFrequency(word);
  88. return frequency >= VALIDITY_THRESHOLD;
  89. }
  90. @Override
  91. public void close() {
  92. flushPendingWrites();
  93. // Don't close the database as locale changes will require it to be reopened anyway
  94. // Also, the database is written to somewhat frequently, so it needs to be kept alive
  95. // throughout the life of the process.
  96. // mOpenHelper.close();
  97. super.close();
  98. }
  99. @Override
  100. public void loadDictionaryAsync() {
  101. // Load the words that correspond to the current input locale
  102. Cursor cursor = query(COLUMN_LOCALE + "=?", new String[] { mLocale });
  103. try {
  104. if (cursor.moveToFirst()) {
  105. int wordIndex = cursor.getColumnIndex(COLUMN_WORD);
  106. int frequencyIndex = cursor.getColumnIndex(COLUMN_FREQUENCY);
  107. while (!cursor.isAfterLast()) {
  108. String word = cursor.getString(wordIndex);
  109. int frequency = cursor.getInt(frequencyIndex);
  110. // Safeguard against adding really long words. Stack may overflow due
  111. // to recursive lookup
  112. if (word.length() < getMaxWordLength()) {
  113. super.addWord(word, frequency);
  114. }
  115. cursor.moveToNext();
  116. }
  117. }
  118. } finally {
  119. cursor.close();
  120. }
  121. }
  122. @Override
  123. public void addWord(String word, int addFrequency) {
  124. final int length = word.length();
  125. // Don't add very short or very long words.
  126. if (length < 2 || length > getMaxWordLength()) return;
  127. if (mIme.getCurrentWord().isAutoCapitalized()) {
  128. // Remove caps before adding
  129. word = Character.toLowerCase(word.charAt(0)) + word.substring(1);
  130. }
  131. int freq = getWordFrequency(word);
  132. freq = freq < 0 ? addFrequency : freq + addFrequency;
  133. super.addWord(word, freq);
  134. if (freq >= PROMOTION_THRESHOLD) {
  135. mIme.promoteToUserDictionary(word, FREQUENCY_FOR_AUTO_ADD);
  136. freq = 0;
  137. }
  138. synchronized (mPendingWritesLock) {
  139. // Write a null frequency if it is to be deleted from the db
  140. mPendingWrites.put(word, freq == 0 ? null : Integer.valueOf(freq));
  141. }
  142. }
  143. /**
  144. * Schedules a background thread to write any pending words to the database.
  145. */
  146. public void flushPendingWrites() {
  147. synchronized (mPendingWritesLock) {
  148. // Nothing pending? Return
  149. if (mPendingWrites.isEmpty()) return;
  150. // Create a background thread to write the pending entries
  151. new UpdateDbTask(getContext(), sOpenHelper, mPendingWrites, mLocale).execute();
  152. // Create a new map for writing new entries into while the old one is written to db
  153. mPendingWrites = new HashMap<String, Integer>();
  154. }
  155. }
  156. /**
  157. * This class helps open, create, and upgrade the database file.
  158. */
  159. private static class DatabaseHelper extends SQLiteOpenHelper {
  160. DatabaseHelper(Context context) {
  161. super(context, DATABASE_NAME, null, DATABASE_VERSION);
  162. }
  163. @Override
  164. public void onCreate(SQLiteDatabase db) {
  165. db.execSQL("CREATE TABLE " + AUTODICT_TABLE_NAME + " ("
  166. + COLUMN_ID + " INTEGER PRIMARY KEY,"
  167. + COLUMN_WORD + " TEXT,"
  168. + COLUMN_FREQUENCY + " INTEGER,"
  169. + COLUMN_LOCALE + " TEXT"
  170. + ");");
  171. }
  172. @Override
  173. public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
  174. Log.w("AutoDictionary", "Upgrading database from version " + oldVersion + " to "
  175. + newVersion + ", which will destroy all old data");
  176. db.execSQL("DROP TABLE IF EXISTS " + AUTODICT_TABLE_NAME);
  177. onCreate(db);
  178. }
  179. }
  180. private Cursor query(String selection, String[] selectionArgs) {
  181. SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
  182. qb.setTables(AUTODICT_TABLE_NAME);
  183. qb.setProjectionMap(sDictProjectionMap);
  184. // Get the database and run the query
  185. SQLiteDatabase db = sOpenHelper.getReadableDatabase();
  186. Cursor c = qb.query(db, null, selection, selectionArgs, null, null,
  187. DEFAULT_SORT_ORDER);
  188. return c;
  189. }
  190. /**
  191. * Async task to write pending words to the database so that it stays in sync with
  192. * the in-memory trie.
  193. */
  194. private static class UpdateDbTask extends AsyncTask<Void, Void, Void> {
  195. private final HashMap<String, Integer> mMap;
  196. private final DatabaseHelper mDbHelper;
  197. private final String mLocale;
  198. public UpdateDbTask(Context context, DatabaseHelper openHelper,
  199. HashMap<String, Integer> pendingWrites, String locale) {
  200. mMap = pendingWrites;
  201. mLocale = locale;
  202. mDbHelper = openHelper;
  203. }
  204. @Override
  205. protected Void doInBackground(Void... v) {
  206. SQLiteDatabase db = mDbHelper.getWritableDatabase();
  207. // Write all the entries to the db
  208. Set<Entry<String,Integer>> mEntries = mMap.entrySet();
  209. for (Entry<String,Integer> entry : mEntries) {
  210. Integer freq = entry.getValue();
  211. db.delete(AUTODICT_TABLE_NAME, COLUMN_WORD + "=? AND " + COLUMN_LOCALE + "=?",
  212. new String[] { entry.getKey(), mLocale });
  213. if (freq != null) {
  214. db.insert(AUTODICT_TABLE_NAME, null,
  215. getContentValues(entry.getKey(), freq, mLocale));
  216. }
  217. }
  218. return null;
  219. }
  220. private ContentValues getContentValues(String word, int frequency, String locale) {
  221. ContentValues values = new ContentValues(4);
  222. values.put(COLUMN_WORD, word);
  223. values.put(COLUMN_FREQUENCY, frequency);
  224. values.put(COLUMN_LOCALE, locale);
  225. return values;
  226. }
  227. }
  228. }