am d60f9c04
: Merge "Implement UserHistoryDictionary for each user account."
* commit 'd60f9c046e6e0ce8d3b04f93506f553f22af6240': Implement UserHistoryDictionary for each user account.
This commit is contained in:
commit
dbb26ae5df
4 changed files with 128 additions and 27 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue