Implement UserHistoryDictionary for each user account.

UserHistoryDictionary currently uses locale to determine the
UserHistoryDictionary on the filesystem. With this change we
use the account name as well. Thus each UserHistoryDictionary
would following the following spec:

UserHistoryDictionary.<locale>.<account>.dict.
In case no account is selected, we default to the existing
spec:
UserHistoryDictionary.<locale>.dict

Example
UserHistoryDictionary.en_US.testaccount@example.com.dict

Bug: 18104749
Change-Id: Iab031e166b55cf2ded68275a7e9be22475737b37
main
Jatin Matani 2014-10-23 17:50:35 -07:00
parent b050b458dc
commit 5365191a9d
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();
}
}