Merge "Implement UserHistoryDictionary for each user account."

main
Jatin Matani 2014-11-14 00:46:36 +00:00 committed by Android (Google) Code Review
commit d60f9c046e
4 changed files with 128 additions and 27 deletions

View File

@ -48,4 +48,10 @@ public final class ProductionFlags {
* When {@code true}, personal dictionary sync feature is ready to be enabled.
*/
public static final boolean ENABLE_PERSONAL_DICTIONARY_SYNC = ENABLE_ACCOUNT_SIGN_IN && false;
/**
* When {@code true}, the IME maintains per account {@link UserHistoryDictionary}.
*/
public static final boolean ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY =
ENABLE_ACCOUNT_SIGN_IN && false;
}

View File

@ -28,32 +28,45 @@ import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Helps handle and manage personalized dictionaries such as {@link UserHistoryDictionary} and
* {@link PersonalizationDictionary}.
*/
public class PersonalizationHelper {
private static final String TAG = PersonalizationHelper.class.getSimpleName();
private static final boolean DEBUG = false;
private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>
sLangUserHistoryDictCache = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, SoftReference<PersonalizationDictionary>>
sLangPersonalizationDictCache = new ConcurrentHashMap<>();
@Nonnull
public static UserHistoryDictionary getUserHistoryDictionary(
final Context context, final Locale locale) {
final String localeStr = locale.toString();
final Context context, final Locale locale, @Nullable final String accountName) {
String lookupStr = locale.toString();
if (accountName != null) {
lookupStr += "." + accountName;
}
synchronized (sLangUserHistoryDictCache) {
if (sLangUserHistoryDictCache.containsKey(localeStr)) {
if (sLangUserHistoryDictCache.containsKey(lookupStr)) {
final SoftReference<UserHistoryDictionary> ref =
sLangUserHistoryDictCache.get(localeStr);
sLangUserHistoryDictCache.get(lookupStr);
final UserHistoryDictionary dict = ref == null ? null : ref.get();
if (dict != null) {
if (DEBUG) {
Log.w(TAG, "Use cached UserHistoryDictionary for " + locale);
Log.d(TAG, "Use cached UserHistoryDictionary for " + locale +
" & account" + accountName);
}
dict.reloadDictionaryIfRequired();
return dict;
}
}
final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale);
sLangUserHistoryDictCache.put(localeStr, new SoftReference<>(dict));
sLangUserHistoryDictCache.put(lookupStr, new SoftReference<>(dict));
return dict;
}
}

View File

@ -17,30 +17,73 @@
package com.android.inputmethod.latin.personalization;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.android.inputmethod.annotations.ExternallyReferenced;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.ExpandableBinaryDictionary;
import com.android.inputmethod.latin.NgramContext;
import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.define.ProductionFlags;
import com.android.inputmethod.latin.settings.LocalSettingsConstants;
import com.android.inputmethod.latin.utils.DistracterFilter;
import java.io.File;
import java.util.Locale;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Locally gathers stats about the words user types and various other signals like auto-correction
* cancellation or manual picks. This allows the keyboard to adapt to the typist over time.
*/
public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBase {
/* package */ static final String NAME = UserHistoryDictionary.class.getSimpleName();
static final String NAME = UserHistoryDictionary.class.getSimpleName();
// TODO: Make this constructor private
/* package */ UserHistoryDictionary(final Context context, final Locale locale) {
super(context, getDictName(NAME, locale, null /* dictFile */), locale,
Dictionary.TYPE_USER_HISTORY, null /* dictFile */);
UserHistoryDictionary(final Context context, final Locale locale) {
super(context,
getUserHistoryDictName(
NAME,
locale,
null /* dictFile */,
context),
locale,
Dictionary.TYPE_USER_HISTORY,
null /* dictFile */);
}
/**
* @returns the name of the {@link UserHistoryDictionary}.
*/
@UsedForTesting
static String getUserHistoryDictName(final String name, final Locale locale,
@Nullable final File dictFile, final Context context) {
if (!ProductionFlags.ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY) {
return getDictName(name, locale, dictFile);
}
return getUserHistoryDictNamePerAccount(name, locale, dictFile, context);
}
/**
* Uses the currently signed in account to determine the dictionary name.
*/
private static String getUserHistoryDictNamePerAccount(final String name, final Locale locale,
@Nullable final File dictFile, final Context context) {
if (dictFile != null) {
return dictFile.getName();
}
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String account = prefs.getString(LocalSettingsConstants.PREF_ACCOUNT_NAME,
null /* default */);
String dictName = name + "." + locale.toString();
if (account != null) {
dictName += "." + account;
}
return dictName;
}
// Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
@ -48,7 +91,14 @@ public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBas
@ExternallyReferenced
public static UserHistoryDictionary getDictionary(final Context context, final Locale locale,
final File dictFile, final String dictNamePrefix) {
return PersonalizationHelper.getUserHistoryDictionary(context, locale);
final String account;
if (ProductionFlags.ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY) {
account = PreferenceManager.getDefaultSharedPreferences(context)
.getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null /* default */);
} else {
account = null;
}
return PersonalizationHelper.getUserHistoryDictionary(context, locale, account);
}
/**

View File

@ -16,6 +16,8 @@
package com.android.inputmethod.latin.personalization;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.LargeTest;
import android.util.Log;
@ -24,6 +26,7 @@ import com.android.inputmethod.latin.ExpandableBinaryDictionary;
import com.android.inputmethod.latin.NgramContext;
import com.android.inputmethod.latin.NgramContext.WordInfo;
import com.android.inputmethod.latin.common.FileUtils;
import com.android.inputmethod.latin.settings.LocalSettingsConstants;
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
import com.android.inputmethod.latin.utils.DistracterFilter;
@ -36,6 +39,8 @@ import java.util.Locale;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
/**
* Unit tests for UserHistoryDictionary
*/
@ -44,6 +49,7 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
private static final String TAG = UserHistoryDictionaryTests.class.getSimpleName();
private static final int WAIT_FOR_WRITING_FILE_IN_MILLISECONDS = 3000;
private static final String TEST_LOCALE_PREFIX = "test_";
private static final String TEST_ACCOUNT = "account@example.com";
private static final String[] CHARACTERS = {
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
@ -52,15 +58,18 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
private int mCurrentTime = 0;
private SharedPreferences mPrefs;
private String mLastKnownAccount = null;
private void removeAllTestDictFiles() {
final Locale dummyLocale = new Locale(TEST_LOCALE_PREFIX);
final String dictName = ExpandableBinaryDictionary.getDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */);
final String dictName = UserHistoryDictionary.getUserHistoryDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
final File dictFile = ExpandableBinaryDictionary.getDictFile(
mContext, dictName, null /* dictFile */);
final FilenameFilter filenameFilter = new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
public boolean accept(final File dir, final String filename) {
return filename.startsWith(UserHistoryDictionary.NAME + "." + TEST_LOCALE_PREFIX);
}
};
@ -99,6 +108,12 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
mPrefs = PreferenceManager.getDefaultSharedPreferences(getContext());
// Keep track of the current account so that we restore it when the test finishes.
mLastKnownAccount = mPrefs.getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null);
updateAccountName(TEST_ACCOUNT);
resetCurrentTimeForTestMode();
removeAllTestDictFiles();
}
@ -107,6 +122,10 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
protected void tearDown() throws Exception {
removeAllTestDictFiles();
stopTestModeInNativeCode();
// Restore the account that was present before running the test.
updateAccountName(mLastKnownAccount);
super.tearDown();
}
@ -115,6 +134,14 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
setCurrentTimeForTestMode(mCurrentTime);
}
private void updateAccountName(@Nullable final String accountName) {
if (accountName == null) {
mPrefs.edit().remove(LocalSettingsConstants.PREF_ACCOUNT_NAME).apply();
} else {
mPrefs.edit().putString(LocalSettingsConstants.PREF_ACCOUNT_NAME, accountName).apply();
}
}
private void forcePassingShortTime() {
// 3 days.
final int timeToElapse = (int)TimeUnit.DAYS.toSeconds(3);
@ -142,7 +169,7 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
*/
private static String generateWord(final int value) {
final int lengthOfChars = CHARACTERS.length;
StringBuilder builder = new StringBuilder();
final StringBuilder builder = new StringBuilder();
long lvalue = Math.abs((long)value);
while (lvalue > 0) {
builder.append(CHARACTERS[(int)(lvalue % lengthOfChars)]);
@ -162,7 +189,7 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
private static void addToDict(final UserHistoryDictionary dict, final List<String> words,
final int timestamp) {
NgramContext ngramContext = NgramContext.EMPTY_PREV_WORDS_INFO;
for (String word : words) {
for (final String word : words) {
UserHistoryDictionary.addToDictionary(dict, ngramContext, word, true, timestamp,
DistracterFilter.EMPTY_DISTRACTER_FILTER);
ngramContext = ngramContext.getNextNgramContext(new WordInfo(word));
@ -204,12 +231,12 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
Log.d(TAG, "This test can be used for profiling.");
Log.d(TAG, "Usage: please set UserHistoryDictionary.PROFILE_SAVE_RESTORE to true.");
final Locale dummyLocale = getDummyLocale("random_words");
final String dictName = ExpandableBinaryDictionary.getDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */);
final String dictName = UserHistoryDictionary.getUserHistoryDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
final File dictFile = ExpandableBinaryDictionary.getDictFile(
mContext, dictName, null /* dictFile */);
final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
getContext(), dummyLocale);
getContext(), dummyLocale, TEST_ACCOUNT);
final int numberOfWords = 1000;
final Random random = new Random(123456);
@ -232,12 +259,12 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
// Create filename suffixes for this test.
for (int i = 0; i < numberOfLanguages; i++) {
final Locale dummyLocale = getDummyLocale("switching_languages" + i);
final String dictName = ExpandableBinaryDictionary.getDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */);
final String dictName = UserHistoryDictionary.getUserHistoryDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
dictFiles[i] = ExpandableBinaryDictionary.getDictFile(
mContext, dictName, null /* dictFile */);
dicts[i] = PersonalizationHelper.getUserHistoryDictionary(getContext(),
dummyLocale);
dummyLocale, TEST_ACCOUNT);
clearHistory(dicts[i]);
}
@ -262,14 +289,14 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
public void testAddManyWords() {
final Locale dummyLocale = getDummyLocale("many_random_words");
final String dictName = ExpandableBinaryDictionary.getDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */);
final String dictName = UserHistoryDictionary.getUserHistoryDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
final File dictFile = ExpandableBinaryDictionary.getDictFile(
mContext, dictName, null /* dictFile */);
final int numberOfWords = 10000;
final Random random = new Random(123456);
final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
getContext(), dummyLocale);
getContext(), dummyLocale, TEST_ACCOUNT);
clearHistory(dict);
try {
addAndWriteRandomWords(dict, numberOfWords, random, true /* checksContents */);
@ -281,7 +308,7 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
public void testDecaying() {
final Locale dummyLocale = getDummyLocale("decaying");
final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
getContext(), dummyLocale);
getContext(), dummyLocale, TEST_ACCOUNT);
final int numberOfWords = 5000;
final Random random = new Random(123456);
resetCurrentTimeForTestMode();
@ -309,4 +336,9 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
assertFalse(dict.isInDictionary(word));
}
}
}
public void testRandomWords_NullAccount() {
updateAccountName(null);
testRandomWords();
}
}