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