Home | History | Annotate | Download | only in latin
      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 
     17 package com.android.inputmethod.latin;
     18 
     19 import java.util.HashMap;
     20 import java.util.Set;
     21 import java.util.Map.Entry;
     22 
     23 import android.content.ContentValues;
     24 import android.content.Context;
     25 import android.database.Cursor;
     26 import android.database.sqlite.SQLiteDatabase;
     27 import android.database.sqlite.SQLiteOpenHelper;
     28 import android.database.sqlite.SQLiteQueryBuilder;
     29 import android.os.AsyncTask;
     30 import android.provider.BaseColumns;
     31 import 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  */
     39 public 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