Save and restore Auto Dictionary data.

Due to the addition of input language switching, the auto dictionary
will be re-created very frequently. We need to save it and restore it
during transitions, otherwise the data will be purged too often to be
of much use in the multi-lingual case.

This also fixes the case where a user frequently turns the phone off/on.
Auto dictionary data was not being saved in those situations either.

The dictionary will probably never grow too large since most of the
frequently used words will be moved to the user dictionary any way.

Also, now the input locale is different from the display locale, so
save the input locale in the UserDictionary class and insert words
into the user dictionary with the correct locale.

And NPE fix for bug: 2464573
main
Amith Yamasani 2010-02-23 19:01:43 -08:00
parent 4ca6d9dc09
commit 6516d0fdfc
6 changed files with 251 additions and 81 deletions

View File

@ -0,0 +1,217 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.android.inputmethod.latin;
import java.util.HashMap;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.provider.BaseColumns;
import android.provider.UserDictionary.Words;
import android.util.Log;
/**
* Stores new words temporarily until they are promoted to the user dictionary
* for longevity. Words in the auto dictionary are used to determine if it's ok
* to accept a word that's not in the main or user dictionary. Using a new word
* repeatedly will promote it to the user dictionary.
*/
public class AutoDictionary extends ExpandableDictionary {
// Weight added to a user picking a new word from the suggestion strip
static final int FREQUENCY_FOR_PICKED = 3;
// Weight added to a user typing a new word that doesn't get corrected (or is reverted)
static final int FREQUENCY_FOR_TYPED = 1;
// A word that is frequently typed and gets promoted to the user dictionary, uses this
// frequency.
static final int FREQUENCY_FOR_AUTO_ADD = 250;
// If the user touches a typed word 2 times or more, it will become valid.
private static final int VALIDITY_THRESHOLD = 2 * FREQUENCY_FOR_PICKED;
// If the user touches a typed word 4 times or more, it will be added to the user dict.
private static final int PROMOTION_THRESHOLD = 4 * FREQUENCY_FOR_PICKED;
private LatinIME mIme;
// Locale for which this auto dictionary is storing words
private String mLocale;
private static final String DATABASE_NAME = "auto_dict.db";
private static final int DATABASE_VERSION = 1;
// These are the columns in the dictionary
// TODO: Consume less space by using a unique id for locale instead of the whole
// 2-5 character string.
private static final String COLUMN_ID = BaseColumns._ID;
private static final String COLUMN_WORD = "word";
private static final String COLUMN_FREQUENCY = "freq";
private static final String COLUMN_LOCALE = "locale";
/** Sort by descending order of frequency. */
public static final String DEFAULT_SORT_ORDER = COLUMN_FREQUENCY + " DESC";
/** Name of the words table in the auto_dict.db */
private static final String AUTODICT_TABLE_NAME = "words";
private static HashMap<String, String> sDictProjectionMap;
static {
sDictProjectionMap = new HashMap<String, String>();
sDictProjectionMap.put(COLUMN_ID, COLUMN_ID);
sDictProjectionMap.put(COLUMN_WORD, COLUMN_WORD);
sDictProjectionMap.put(COLUMN_FREQUENCY, COLUMN_FREQUENCY);
sDictProjectionMap.put(COLUMN_LOCALE, COLUMN_LOCALE);
}
private DatabaseHelper mOpenHelper;
public AutoDictionary(Context context, LatinIME ime, String locale) {
super(context);
mIme = ime;
mLocale = locale;
mOpenHelper = new DatabaseHelper(getContext());
if (mLocale != null && mLocale.length() > 1) {
loadDictionary();
}
}
@Override
public boolean isValidWord(CharSequence word) {
final int frequency = getWordFrequency(word);
return frequency >= VALIDITY_THRESHOLD;
}
public void close() {
mOpenHelper.close();
}
private void loadDictionary() {
// Load the words that correspond to the current input locale
Cursor cursor = query(COLUMN_LOCALE + "=?", new String[] { mLocale });
if (cursor.moveToFirst()) {
int wordIndex = cursor.getColumnIndex(COLUMN_WORD);
int frequencyIndex = cursor.getColumnIndex(COLUMN_FREQUENCY);
while (!cursor.isAfterLast()) {
String word = cursor.getString(wordIndex);
int frequency = cursor.getInt(frequencyIndex);
// Safeguard against adding really long words. Stack may overflow due
// to recursive lookup
if (word.length() < getMaxWordLength()) {
super.addWord(word, frequency);
}
cursor.moveToNext();
}
}
cursor.close();
}
@Override
public void addWord(String word, int addFrequency) {
final int length = word.length();
// Don't add very short or very long words.
if (length < 2 || length > getMaxWordLength()) return;
if (mIme.getCurrentWord().isAutoCapitalized()) {
// Remove caps before adding
word = Character.toLowerCase(word.charAt(0)) + word.substring(1);
}
int freq = getWordFrequency(word);
freq = freq < 0 ? addFrequency : freq + addFrequency;
super.addWord(word, freq);
if (freq >= PROMOTION_THRESHOLD) {
mIme.promoteToUserDictionary(word, FREQUENCY_FOR_AUTO_ADD);
// Delete the word (for input locale) from the auto dictionary db, as it
// is now in the user dictionary provider.
delete(COLUMN_WORD + "=? AND " + COLUMN_LOCALE + "=?",
new String[] { word, mLocale });
} else {
update(word, freq, mLocale);
}
}
/**
* This class helps open, create, and upgrade the database file.
*/
private static class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + AUTODICT_TABLE_NAME + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
+ COLUMN_WORD + " TEXT,"
+ COLUMN_FREQUENCY + " INTEGER,"
+ COLUMN_LOCALE + " TEXT"
+ ");");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w("AutoDictionary", "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + AUTODICT_TABLE_NAME);
onCreate(db);
}
}
private Cursor query(String selection, String[] selectionArgs) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(AUTODICT_TABLE_NAME);
qb.setProjectionMap(sDictProjectionMap);
// Get the database and run the query
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
Cursor c = qb.query(db, null, selection, selectionArgs, null, null,
DEFAULT_SORT_ORDER);
return c;
}
private boolean insert(ContentValues values) {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
long rowId = db.insert(AUTODICT_TABLE_NAME, Words.WORD, values);
if (rowId > 0) {
return true;
}
return false;
}
private int delete(String where, String[] whereArgs) {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count = db.delete(AUTODICT_TABLE_NAME, where, whereArgs);
return count;
}
private int update(String word, int frequency, String locale) {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
long count = db.delete(AUTODICT_TABLE_NAME, COLUMN_WORD + "=? AND " + COLUMN_LOCALE + "=?",
new String[] { word, locale });
count = db.insert(AUTODICT_TABLE_NAME, null,
getContentValues(word, frequency, locale));
return (int) count;
}
private ContentValues getContentValues(String word, int frequency, String locale) {
ContentValues values = new ContentValues(4);
values.put(COLUMN_WORD, word);
values.put(COLUMN_FREQUENCY, frequency);
values.put(COLUMN_LOCALE, locale);
return values;
}
}

View File

@ -19,7 +19,6 @@ package com.android.inputmethod.latin;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import android.content.SharedPreferences;
@ -29,7 +28,7 @@ import android.preference.CheckBoxPreference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.util.Log;
import android.text.TextUtils;
public class InputLanguageSelection extends PreferenceActivity {
@ -91,7 +90,9 @@ public class InputLanguageSelection extends PreferenceActivity {
}
private String get5Code(Locale locale) {
return locale.getLanguage() + "_" + locale.getCountry();
String country = locale.getCountry();
return locale.getLanguage()
+ (TextUtils.isEmpty(country) ? "" : "_" + country);
}
@Override

View File

@ -180,6 +180,7 @@ public class KeyboardSwitcher {
void setKeyboardMode(int mode, int imeOptions,
boolean enableVoice, boolean isSymbols) {
if (mInputView == null) return;
mMode = mode;
mImeOptions = imeOptions;
mHasVoice = enableVoice;

View File

@ -21,6 +21,7 @@ import java.util.Locale;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.preference.PreferenceManager;
import android.text.TextUtils;
/**
* Keeps track of list of selected input languages and the current
@ -88,8 +89,9 @@ public class LanguageSwitcher {
private void loadDefaults() {
mDefaultInputLocale = mIme.getResources().getConfiguration().locale;
mDefaultInputLanguage = mDefaultInputLocale.getLanguage() + "_"
+ mDefaultInputLocale.getCountry();
String country = mDefaultInputLocale.getCountry();
mDefaultInputLanguage = mDefaultInputLocale.getLanguage() +
(TextUtils.isEmpty(country) ? "" : "_" + country);
}
private void constructLocales() {

View File

@ -130,21 +130,10 @@ public class LatinIME extends InputMethodService
// ignored, since it may in fact be two key presses in quick succession.
private static final long MIN_MILLIS_AFTER_TYPING_BEFORE_SWIPE = 1000;
// If we detect a swipe gesture, and the user types N ms later, cancel the
// swipe since it was probably a false trigger.
private static final long MIN_MILLIS_AFTER_SWIPE_TO_WAIT_FOR_TYPING = 500;
// How many continuous deletes at which to start deleting at a higher speed.
private static final int DELETE_ACCELERATE_AT = 20;
// Key events coming any faster than this are long-presses.
private static final int QUICK_PRESS = 200;
// Weight added to a user picking a new word from the suggestion strip
static final int FREQUENCY_FOR_PICKED = 3;
// Weight added to a user typing a new word that doesn't get corrected (or is reverted)
static final int FREQUENCY_FOR_TYPED = 1;
// A word that is frequently typed and get's promoted to the user dictionary, uses this
// frequency.
static final int FREQUENCY_FOR_AUTO_ADD = 250;
static final int KEYCODE_ENTER = '\n';
static final int KEYCODE_SPACE = ' ';
@ -329,14 +318,14 @@ public class LatinIME extends InputMethodService
mSuggest = new Suggest(this, R.raw.main);
updateAutoTextEnabled(saveLocale);
if (mUserDictionary != null) mUserDictionary.close();
mUserDictionary = new UserDictionary(this);
mUserDictionary = new UserDictionary(this, mLocale);
if (mContactsDictionary == null) {
mContactsDictionary = new ContactsDictionary(this);
}
// TODO: Save and restore the dictionary for the current input language.
if (mAutoDictionary == null) {
mAutoDictionary = new AutoDictionary(this);
if (mAutoDictionary != null) {
mAutoDictionary.close();
}
mAutoDictionary = new AutoDictionary(this, this, mLocale);
mSuggest.setUserDictionary(mUserDictionary);
mSuggest.setContactsDictionary(mContactsDictionary);
mSuggest.setAutoDictionary(mAutoDictionary);
@ -815,7 +804,7 @@ public class LatinIME extends InputMethodService
}
mCommittedLength = mComposing.length();
TextEntryState.acceptedTyped(mComposing);
checkAddToDictionary(mComposing, FREQUENCY_FOR_TYPED);
checkAddToDictionary(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED);
}
updateSuggestions();
}
@ -1196,7 +1185,6 @@ public class LatinIME extends InputMethodService
private boolean isPredictionOn() {
boolean predictionOn = mPredictionOn;
//if (isFullscreenMode()) predictionOn &= mPredictionLandscape;
return predictionOn;
}
@ -1522,7 +1510,7 @@ public class LatinIME extends InputMethodService
}
}
// Add the word to the auto dictionary if it's not a known word
checkAddToDictionary(suggestion, FREQUENCY_FOR_PICKED);
checkAddToDictionary(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED);
mPredicting = false;
mCommittedLength = suggestion.length();
setNextSuggestions();
@ -1683,11 +1671,6 @@ public class LatinIME extends InputMethodService
&& !mVoiceInput.isBlacklistedField(fieldContext);
}
private boolean fieldIsRecommendedForVoice(FieldContext fieldContext) {
// TODO: Move this logic into the VoiceInput method.
return !mPasswordText && !mEmailText && mVoiceInput.isRecommendedField(fieldContext);
}
private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) {
return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext)
&& !(attribute != null && attribute.privateImeOptions != null
@ -1718,21 +1701,6 @@ public class LatinIME extends InputMethodService
> MIN_MILLIS_AFTER_TYPING_BEFORE_SWIPE;
}
/*
* Only trigger a swipe action if the user hasn't typed X millis before
* now, and if they don't type Y millis after the swipe is detected. This
* delays the onset of the swipe action by Y millis.
*/
private void conservativelyTriggerSwipeAction(final Runnable action) {
if (userHasNotTypedRecently()) {
mSwipeTriggerTimeMillis = System.currentTimeMillis();
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_START_LISTENING_AFTER_SWIPE),
MIN_MILLIS_AFTER_SWIPE_TO_WAIT_FOR_TYPING);
}
}
private void playKeyClick(int primaryCode) {
// if mAudioManager is null, we don't have the ringer state yet
// mAudioManager will be set by updateRingerMode
@ -1796,6 +1764,10 @@ public class LatinIME extends InputMethodService
mUserDictionary.addWord(word, frequency);
}
WordComposer getCurrentWord() {
return mWord;
}
private void updateCorrectionMode() {
mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false;
mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes)
@ -1972,38 +1944,4 @@ public class LatinIME extends InputMethodService
System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total));
}
class AutoDictionary extends ExpandableDictionary {
// If the user touches a typed word 2 times or more, it will become valid.
private static final int VALIDITY_THRESHOLD = 2 * FREQUENCY_FOR_PICKED;
// If the user touches a typed word 5 times or more, it will be added to the user dict.
private static final int PROMOTION_THRESHOLD = 4 * FREQUENCY_FOR_PICKED;
public AutoDictionary(Context context) {
super(context);
}
@Override
public boolean isValidWord(CharSequence word) {
final int frequency = getWordFrequency(word);
return frequency >= VALIDITY_THRESHOLD;
}
@Override
public void addWord(String word, int addFrequency) {
final int length = word.length();
// Don't add very short or very long words.
if (length < 2 || length > getMaxWordLength()) return;
if (mWord.isAutoCapitalized()) {
// Remove caps before adding
word = Character.toLowerCase(word.charAt(0))
+ word.substring(1);
}
int freq = getWordFrequency(word);
freq = freq < 0 ? addFrequency : freq + addFrequency;
super.addWord(word, freq);
if (freq >= PROMOTION_THRESHOLD) {
LatinIME.this.promoteToUserDictionary(word, FREQUENCY_FOR_AUTO_ADD);
}
}
}
}

View File

@ -21,9 +21,11 @@ import java.util.List;
import java.util.Locale;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.provider.UserDictionary.Words;
public class UserDictionary extends ExpandableDictionary {
@ -40,9 +42,11 @@ public class UserDictionary extends ExpandableDictionary {
private ContentObserver mObserver;
private boolean mRequiresReload;
public UserDictionary(Context context) {
private String mLocale;
public UserDictionary(Context context, String locale) {
super(context);
mLocale = locale;
// Perform a managed query. The Activity will handle closing and requerying the cursor
// when needed.
ContentResolver cres = context.getContentResolver();
@ -67,7 +71,7 @@ public class UserDictionary extends ExpandableDictionary {
private synchronized void loadDictionary() {
Cursor cursor = getContext().getContentResolver()
.query(Words.CONTENT_URI, PROJECTION, "(locale IS NULL) or (locale=?)",
new String[] { Locale.getDefault().toString() }, null);
new String[] { mLocale }, null);
addWords(cursor);
mRequiresReload = false;
}
@ -88,7 +92,14 @@ public class UserDictionary extends ExpandableDictionary {
super.addWord(word, frequency);
Words.addWord(getContext(), word, frequency, Words.LOCALE_TYPE_CURRENT);
// Update the user dictionary provider
ContentValues values = new ContentValues(5);
values.put(Words.WORD, word);
values.put(Words.FREQUENCY, frequency);
values.put(Words.LOCALE, mLocale);
values.put(Words.APP_ID, 0);
getContext().getContentResolver().insert(Words.CONTENT_URI, values);
// In case the above does a synchronous callback of the change observer
mRequiresReload = false;
}