am d60f9c04: Merge "Implement UserHistoryDictionary for each user account."

* commit 'd60f9c046e6e0ce8d3b04f93506f553f22af6240':
  Implement UserHistoryDictionary for each user account.
main
Jatin Matani 2014-11-14 00:50:32 +00:00 committed by Android Git Automerger
commit dbb26ae5df
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. * 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; 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.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; 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 { public class PersonalizationHelper {
private static final String TAG = PersonalizationHelper.class.getSimpleName(); private static final String TAG = PersonalizationHelper.class.getSimpleName();
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>> private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>
sLangUserHistoryDictCache = new ConcurrentHashMap<>(); sLangUserHistoryDictCache = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, SoftReference<PersonalizationDictionary>> private static final ConcurrentHashMap<String, SoftReference<PersonalizationDictionary>>
sLangPersonalizationDictCache = new ConcurrentHashMap<>(); sLangPersonalizationDictCache = new ConcurrentHashMap<>();
@Nonnull
public static UserHistoryDictionary getUserHistoryDictionary( public static UserHistoryDictionary getUserHistoryDictionary(
final Context context, final Locale locale) { final Context context, final Locale locale, @Nullable final String accountName) {
final String localeStr = locale.toString(); String lookupStr = locale.toString();
if (accountName != null) {
lookupStr += "." + accountName;
}
synchronized (sLangUserHistoryDictCache) { synchronized (sLangUserHistoryDictCache) {
if (sLangUserHistoryDictCache.containsKey(localeStr)) { if (sLangUserHistoryDictCache.containsKey(lookupStr)) {
final SoftReference<UserHistoryDictionary> ref = final SoftReference<UserHistoryDictionary> ref =
sLangUserHistoryDictCache.get(localeStr); sLangUserHistoryDictCache.get(lookupStr);
final UserHistoryDictionary dict = ref == null ? null : ref.get(); final UserHistoryDictionary dict = ref == null ? null : ref.get();
if (dict != null) { if (dict != null) {
if (DEBUG) { if (DEBUG) {
Log.w(TAG, "Use cached UserHistoryDictionary for " + locale); Log.d(TAG, "Use cached UserHistoryDictionary for " + locale +
" & account" + accountName);
} }
dict.reloadDictionaryIfRequired(); dict.reloadDictionaryIfRequired();
return dict; return dict;
} }
} }
final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale); final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale);
sLangUserHistoryDictCache.put(localeStr, new SoftReference<>(dict)); sLangUserHistoryDictCache.put(lookupStr, new SoftReference<>(dict));
return dict; return dict;
} }
} }

View File

@ -17,30 +17,73 @@
package com.android.inputmethod.latin.personalization; package com.android.inputmethod.latin.personalization;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import com.android.inputmethod.annotations.ExternallyReferenced; import com.android.inputmethod.annotations.ExternallyReferenced;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.ExpandableBinaryDictionary; import com.android.inputmethod.latin.ExpandableBinaryDictionary;
import com.android.inputmethod.latin.NgramContext; import com.android.inputmethod.latin.NgramContext;
import com.android.inputmethod.latin.common.Constants; 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 com.android.inputmethod.latin.utils.DistracterFilter;
import java.io.File; import java.io.File;
import java.util.Locale; import java.util.Locale;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/** /**
* Locally gathers stats about the words user types and various other signals like auto-correction * 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. * cancellation or manual picks. This allows the keyboard to adapt to the typist over time.
*/ */
public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBase { 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 // TODO: Make this constructor private
/* package */ UserHistoryDictionary(final Context context, final Locale locale) { UserHistoryDictionary(final Context context, final Locale locale) {
super(context, getDictName(NAME, locale, null /* dictFile */), locale, super(context,
Dictionary.TYPE_USER_HISTORY, null /* dictFile */); 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. // Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
@ -48,7 +91,14 @@ public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBas
@ExternallyReferenced @ExternallyReferenced
public static UserHistoryDictionary getDictionary(final Context context, final Locale locale, public static UserHistoryDictionary getDictionary(final Context context, final Locale locale,
final File dictFile, final String dictNamePrefix) { 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; package com.android.inputmethod.latin.personalization;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.LargeTest;
import android.util.Log; 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;
import com.android.inputmethod.latin.NgramContext.WordInfo; import com.android.inputmethod.latin.NgramContext.WordInfo;
import com.android.inputmethod.latin.common.FileUtils; 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.BinaryDictionaryUtils;
import com.android.inputmethod.latin.utils.DistracterFilter; import com.android.inputmethod.latin.utils.DistracterFilter;
@ -36,6 +39,8 @@ import java.util.Locale;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
/** /**
* Unit tests for UserHistoryDictionary * Unit tests for UserHistoryDictionary
*/ */
@ -44,6 +49,7 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
private static final String TAG = UserHistoryDictionaryTests.class.getSimpleName(); private static final String TAG = UserHistoryDictionaryTests.class.getSimpleName();
private static final int WAIT_FOR_WRITING_FILE_IN_MILLISECONDS = 3000; private static final int WAIT_FOR_WRITING_FILE_IN_MILLISECONDS = 3000;
private static final String TEST_LOCALE_PREFIX = "test_"; private static final String TEST_LOCALE_PREFIX = "test_";
private static final String TEST_ACCOUNT = "account@example.com";
private static final String[] CHARACTERS = { private static final String[] CHARACTERS = {
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "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 int mCurrentTime = 0;
private SharedPreferences mPrefs;
private String mLastKnownAccount = null;
private void removeAllTestDictFiles() { private void removeAllTestDictFiles() {
final Locale dummyLocale = new Locale(TEST_LOCALE_PREFIX); final Locale dummyLocale = new Locale(TEST_LOCALE_PREFIX);
final String dictName = ExpandableBinaryDictionary.getDictName( final String dictName = UserHistoryDictionary.getUserHistoryDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */); UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
final File dictFile = ExpandableBinaryDictionary.getDictFile( final File dictFile = ExpandableBinaryDictionary.getDictFile(
mContext, dictName, null /* dictFile */); mContext, dictName, null /* dictFile */);
final FilenameFilter filenameFilter = new FilenameFilter() { final FilenameFilter filenameFilter = new FilenameFilter() {
@Override @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); return filename.startsWith(UserHistoryDictionary.NAME + "." + TEST_LOCALE_PREFIX);
} }
}; };
@ -99,6 +108,12 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); 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(); resetCurrentTimeForTestMode();
removeAllTestDictFiles(); removeAllTestDictFiles();
} }
@ -107,6 +122,10 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
protected void tearDown() throws Exception { protected void tearDown() throws Exception {
removeAllTestDictFiles(); removeAllTestDictFiles();
stopTestModeInNativeCode(); stopTestModeInNativeCode();
// Restore the account that was present before running the test.
updateAccountName(mLastKnownAccount);
super.tearDown(); super.tearDown();
} }
@ -115,6 +134,14 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
setCurrentTimeForTestMode(mCurrentTime); 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() { private void forcePassingShortTime() {
// 3 days. // 3 days.
final int timeToElapse = (int)TimeUnit.DAYS.toSeconds(3); final int timeToElapse = (int)TimeUnit.DAYS.toSeconds(3);
@ -142,7 +169,7 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
*/ */
private static String generateWord(final int value) { private static String generateWord(final int value) {
final int lengthOfChars = CHARACTERS.length; final int lengthOfChars = CHARACTERS.length;
StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();
long lvalue = Math.abs((long)value); long lvalue = Math.abs((long)value);
while (lvalue > 0) { while (lvalue > 0) {
builder.append(CHARACTERS[(int)(lvalue % lengthOfChars)]); 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, private static void addToDict(final UserHistoryDictionary dict, final List<String> words,
final int timestamp) { final int timestamp) {
NgramContext ngramContext = NgramContext.EMPTY_PREV_WORDS_INFO; NgramContext ngramContext = NgramContext.EMPTY_PREV_WORDS_INFO;
for (String word : words) { for (final String word : words) {
UserHistoryDictionary.addToDictionary(dict, ngramContext, word, true, timestamp, UserHistoryDictionary.addToDictionary(dict, ngramContext, word, true, timestamp,
DistracterFilter.EMPTY_DISTRACTER_FILTER); DistracterFilter.EMPTY_DISTRACTER_FILTER);
ngramContext = ngramContext.getNextNgramContext(new WordInfo(word)); 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, "This test can be used for profiling.");
Log.d(TAG, "Usage: please set UserHistoryDictionary.PROFILE_SAVE_RESTORE to true."); Log.d(TAG, "Usage: please set UserHistoryDictionary.PROFILE_SAVE_RESTORE to true.");
final Locale dummyLocale = getDummyLocale("random_words"); final Locale dummyLocale = getDummyLocale("random_words");
final String dictName = ExpandableBinaryDictionary.getDictName( final String dictName = UserHistoryDictionary.getUserHistoryDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */); UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
final File dictFile = ExpandableBinaryDictionary.getDictFile( final File dictFile = ExpandableBinaryDictionary.getDictFile(
mContext, dictName, null /* dictFile */); mContext, dictName, null /* dictFile */);
final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary( final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
getContext(), dummyLocale); getContext(), dummyLocale, TEST_ACCOUNT);
final int numberOfWords = 1000; final int numberOfWords = 1000;
final Random random = new Random(123456); final Random random = new Random(123456);
@ -232,12 +259,12 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
// Create filename suffixes for this test. // Create filename suffixes for this test.
for (int i = 0; i < numberOfLanguages; i++) { for (int i = 0; i < numberOfLanguages; i++) {
final Locale dummyLocale = getDummyLocale("switching_languages" + i); final Locale dummyLocale = getDummyLocale("switching_languages" + i);
final String dictName = ExpandableBinaryDictionary.getDictName( final String dictName = UserHistoryDictionary.getUserHistoryDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */); UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
dictFiles[i] = ExpandableBinaryDictionary.getDictFile( dictFiles[i] = ExpandableBinaryDictionary.getDictFile(
mContext, dictName, null /* dictFile */); mContext, dictName, null /* dictFile */);
dicts[i] = PersonalizationHelper.getUserHistoryDictionary(getContext(), dicts[i] = PersonalizationHelper.getUserHistoryDictionary(getContext(),
dummyLocale); dummyLocale, TEST_ACCOUNT);
clearHistory(dicts[i]); clearHistory(dicts[i]);
} }
@ -262,14 +289,14 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
public void testAddManyWords() { public void testAddManyWords() {
final Locale dummyLocale = getDummyLocale("many_random_words"); final Locale dummyLocale = getDummyLocale("many_random_words");
final String dictName = ExpandableBinaryDictionary.getDictName( final String dictName = UserHistoryDictionary.getUserHistoryDictName(
UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */); UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
final File dictFile = ExpandableBinaryDictionary.getDictFile( final File dictFile = ExpandableBinaryDictionary.getDictFile(
mContext, dictName, null /* dictFile */); mContext, dictName, null /* dictFile */);
final int numberOfWords = 10000; final int numberOfWords = 10000;
final Random random = new Random(123456); final Random random = new Random(123456);
final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary( final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
getContext(), dummyLocale); getContext(), dummyLocale, TEST_ACCOUNT);
clearHistory(dict); clearHistory(dict);
try { try {
addAndWriteRandomWords(dict, numberOfWords, random, true /* checksContents */); addAndWriteRandomWords(dict, numberOfWords, random, true /* checksContents */);
@ -281,7 +308,7 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
public void testDecaying() { public void testDecaying() {
final Locale dummyLocale = getDummyLocale("decaying"); final Locale dummyLocale = getDummyLocale("decaying");
final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary( final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
getContext(), dummyLocale); getContext(), dummyLocale, TEST_ACCOUNT);
final int numberOfWords = 5000; final int numberOfWords = 5000;
final Random random = new Random(123456); final Random random = new Random(123456);
resetCurrentTimeForTestMode(); resetCurrentTimeForTestMode();
@ -309,4 +336,9 @@ public class UserHistoryDictionaryTests extends AndroidTestCase {
assertFalse(dict.isInDictionary(word)); assertFalse(dict.isInDictionary(word));
} }
} }
}
public void testRandomWords_NullAccount() {
updateAccountName(null);
testRandomWords();
}
}