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