2013-12-26 11:28:37 +00:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2013 The Android Open Source Project
|
|
|
|
*
|
|
|
|
* 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 android.content.Context;
|
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.util.Log;
|
2014-05-23 07:10:36 +00:00
|
|
|
import android.view.inputmethod.InputMethodSubtype;
|
2013-12-26 11:28:37 +00:00
|
|
|
|
|
|
|
import com.android.inputmethod.annotations.UsedForTesting;
|
|
|
|
import com.android.inputmethod.keyboard.ProximityInfo;
|
|
|
|
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
2014-04-28 13:01:51 +00:00
|
|
|
import com.android.inputmethod.latin.personalization.ContextualDictionary;
|
2014-05-23 07:10:36 +00:00
|
|
|
import com.android.inputmethod.latin.personalization.PersonalizationDataChunk;
|
2014-05-01 08:22:23 +00:00
|
|
|
import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
|
2013-12-26 11:28:37 +00:00
|
|
|
import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
|
2014-05-23 07:10:36 +00:00
|
|
|
import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
|
|
|
|
import com.android.inputmethod.latin.utils.DistracterFilter;
|
2014-06-08 22:55:27 +00:00
|
|
|
import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary;
|
2014-02-24 12:05:24 +00:00
|
|
|
import com.android.inputmethod.latin.utils.ExecutorUtils;
|
2014-01-16 01:54:17 +00:00
|
|
|
import com.android.inputmethod.latin.utils.LanguageModelParam;
|
2014-03-25 06:35:20 +00:00
|
|
|
import com.android.inputmethod.latin.utils.SuggestionResults;
|
2013-12-26 11:28:37 +00:00
|
|
|
|
2013-12-26 09:26:28 +00:00
|
|
|
import java.io.File;
|
2014-05-01 09:44:57 +00:00
|
|
|
import java.lang.reflect.InvocationTargetException;
|
|
|
|
import java.lang.reflect.Method;
|
2013-12-26 09:26:28 +00:00
|
|
|
import java.util.ArrayList;
|
2014-05-01 06:51:03 +00:00
|
|
|
import java.util.Arrays;
|
2013-12-26 09:26:28 +00:00
|
|
|
import java.util.HashMap;
|
2014-05-23 11:18:17 +00:00
|
|
|
import java.util.HashSet;
|
2014-05-23 07:10:36 +00:00
|
|
|
import java.util.List;
|
2013-12-26 11:28:37 +00:00
|
|
|
import java.util.Locale;
|
2014-02-26 10:14:16 +00:00
|
|
|
import java.util.Map;
|
2013-12-26 11:28:37 +00:00
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
2014-01-08 09:59:43 +00:00
|
|
|
import java.util.concurrent.CountDownLatch;
|
2013-12-26 11:28:37 +00:00
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
// TODO: Consolidate dictionaries in native code.
|
2014-05-23 00:30:55 +00:00
|
|
|
public class DictionaryFacilitator {
|
|
|
|
public static final String TAG = DictionaryFacilitator.class.getSimpleName();
|
2013-12-26 11:28:37 +00:00
|
|
|
|
2014-01-28 07:31:37 +00:00
|
|
|
// HACK: This threshold is being used when adding a capitalized entry in the User History
|
|
|
|
// dictionary.
|
|
|
|
private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140;
|
|
|
|
|
2014-03-25 06:35:20 +00:00
|
|
|
private Dictionaries mDictionaries = new Dictionaries();
|
2014-04-30 11:35:08 +00:00
|
|
|
private boolean mIsUserDictEnabled = false;
|
2014-03-25 06:35:20 +00:00
|
|
|
private volatile CountDownLatch mLatchForWaitingLoadingMainDictionary = new CountDownLatch(0);
|
|
|
|
// To synchronize assigning mDictionaries to ensure closing dictionaries.
|
2014-05-12 09:04:43 +00:00
|
|
|
private final Object mLock = new Object();
|
2014-05-23 07:10:36 +00:00
|
|
|
private final DistracterFilter mDistracterFilter;
|
2013-12-26 11:28:37 +00:00
|
|
|
|
2014-06-05 10:05:58 +00:00
|
|
|
private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS =
|
2014-04-02 13:17:18 +00:00
|
|
|
new String[] {
|
|
|
|
Dictionary.TYPE_MAIN,
|
|
|
|
Dictionary.TYPE_USER_HISTORY,
|
|
|
|
Dictionary.TYPE_PERSONALIZATION,
|
|
|
|
Dictionary.TYPE_USER,
|
2014-04-28 13:01:51 +00:00
|
|
|
Dictionary.TYPE_CONTACTS,
|
|
|
|
Dictionary.TYPE_CONTEXTUAL
|
2014-04-02 13:17:18 +00:00
|
|
|
};
|
|
|
|
|
2014-06-10 10:59:19 +00:00
|
|
|
public static final Map<String, Class<? extends ExpandableBinaryDictionary>>
|
2014-05-23 11:18:17 +00:00
|
|
|
DICT_TYPE_TO_CLASS = new HashMap<>();
|
2014-05-01 09:44:57 +00:00
|
|
|
|
|
|
|
static {
|
|
|
|
DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER_HISTORY, UserHistoryDictionary.class);
|
|
|
|
DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_PERSONALIZATION, PersonalizationDictionary.class);
|
|
|
|
DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER, UserBinaryDictionary.class);
|
|
|
|
DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTACTS, ContactsBinaryDictionary.class);
|
2014-04-28 13:01:51 +00:00
|
|
|
DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTEXTUAL, ContextualDictionary.class);
|
2014-05-01 09:44:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private static final String DICT_FACTORY_METHOD_NAME = "getDictionary";
|
|
|
|
private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES =
|
2014-06-10 09:05:10 +00:00
|
|
|
new Class[] { Context.class, Locale.class, File.class, String.class };
|
2014-05-01 09:44:57 +00:00
|
|
|
|
2014-05-01 06:51:03 +00:00
|
|
|
private static final String[] SUB_DICT_TYPES =
|
2014-06-05 10:05:58 +00:00
|
|
|
Arrays.copyOfRange(DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS, 1 /* start */,
|
|
|
|
DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS.length);
|
2014-05-01 06:51:03 +00:00
|
|
|
|
2014-03-25 06:35:20 +00:00
|
|
|
/**
|
|
|
|
* Class contains dictionaries for a locale.
|
|
|
|
*/
|
|
|
|
private static class Dictionaries {
|
|
|
|
public final Locale mLocale;
|
2014-04-28 13:09:00 +00:00
|
|
|
private Dictionary mMainDict;
|
2014-04-23 20:13:56 +00:00
|
|
|
public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap =
|
2014-05-23 11:18:17 +00:00
|
|
|
new ConcurrentHashMap<>();
|
2014-03-25 06:35:20 +00:00
|
|
|
|
|
|
|
public Dictionaries() {
|
|
|
|
mLocale = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public Dictionaries(final Locale locale, final Dictionary mainDict,
|
2014-05-01 06:51:03 +00:00
|
|
|
final Map<String, ExpandableBinaryDictionary> subDicts) {
|
2014-03-25 06:35:20 +00:00
|
|
|
mLocale = locale;
|
2014-04-23 20:13:56 +00:00
|
|
|
// Main dictionary can be asynchronously loaded.
|
2014-03-25 06:35:20 +00:00
|
|
|
setMainDict(mainDict);
|
2014-05-01 06:51:03 +00:00
|
|
|
for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) {
|
|
|
|
setSubDict(entry.getKey(), entry.getValue());
|
|
|
|
}
|
2014-04-23 20:13:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) {
|
|
|
|
if (dict != null) {
|
|
|
|
mSubDictMap.put(dictType, dict);
|
2014-03-25 06:35:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setMainDict(final Dictionary mainDict) {
|
|
|
|
// Close old dictionary if exists. Main dictionary can be assigned multiple times.
|
2014-04-28 13:09:00 +00:00
|
|
|
final Dictionary oldDict = mMainDict;
|
|
|
|
mMainDict = mainDict;
|
2014-04-23 20:13:56 +00:00
|
|
|
if (oldDict != null && mainDict != oldDict) {
|
2014-03-25 06:35:20 +00:00
|
|
|
oldDict.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-04-28 13:09:00 +00:00
|
|
|
public Dictionary getDict(final String dictType) {
|
|
|
|
if (Dictionary.TYPE_MAIN.equals(dictType)) {
|
|
|
|
return mMainDict;
|
|
|
|
} else {
|
|
|
|
return getSubDict(dictType);
|
|
|
|
}
|
2014-03-25 06:35:20 +00:00
|
|
|
}
|
2013-12-26 11:28:37 +00:00
|
|
|
|
2014-04-23 20:13:56 +00:00
|
|
|
public ExpandableBinaryDictionary getSubDict(final String dictType) {
|
|
|
|
return mSubDictMap.get(dictType);
|
2014-03-25 06:35:20 +00:00
|
|
|
}
|
|
|
|
|
2014-04-23 20:13:56 +00:00
|
|
|
public boolean hasDict(final String dictType) {
|
2014-04-28 13:09:00 +00:00
|
|
|
if (Dictionary.TYPE_MAIN.equals(dictType)) {
|
|
|
|
return mMainDict != null;
|
|
|
|
} else {
|
|
|
|
return mSubDictMap.containsKey(dictType);
|
|
|
|
}
|
2014-03-25 06:35:20 +00:00
|
|
|
}
|
|
|
|
|
2014-04-23 20:13:56 +00:00
|
|
|
public void closeDict(final String dictType) {
|
2014-04-28 13:09:00 +00:00
|
|
|
final Dictionary dict;
|
|
|
|
if (Dictionary.TYPE_MAIN.equals(dictType)) {
|
|
|
|
dict = mMainDict;
|
|
|
|
} else {
|
|
|
|
dict = mSubDictMap.remove(dictType);
|
|
|
|
}
|
2014-04-23 20:13:56 +00:00
|
|
|
if (dict != null) {
|
|
|
|
dict.close();
|
|
|
|
}
|
2014-03-25 06:35:20 +00:00
|
|
|
}
|
|
|
|
}
|
2013-12-26 11:28:37 +00:00
|
|
|
|
2013-12-27 05:49:32 +00:00
|
|
|
public interface DictionaryInitializationListener {
|
|
|
|
public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable);
|
|
|
|
}
|
|
|
|
|
2014-05-23 07:10:36 +00:00
|
|
|
public DictionaryFacilitator() {
|
2014-05-26 08:28:27 +00:00
|
|
|
mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER;
|
2014-05-23 07:10:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public DictionaryFacilitator(final DistracterFilter distracterFilter) {
|
|
|
|
mDistracterFilter = distracterFilter;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) {
|
|
|
|
mDistracterFilter.updateEnabledSubtypes(enabledSubtypes);
|
|
|
|
}
|
2014-03-25 06:35:20 +00:00
|
|
|
|
|
|
|
public Locale getLocale() {
|
|
|
|
return mDictionaries.mLocale;
|
2013-12-27 05:49:32 +00:00
|
|
|
}
|
|
|
|
|
2014-05-01 06:51:03 +00:00
|
|
|
private static ExpandableBinaryDictionary getSubDict(final String dictType,
|
2014-06-10 09:05:10 +00:00
|
|
|
final Context context, final Locale locale, final File dictFile,
|
|
|
|
final String dictNamePrefix) {
|
2014-05-01 09:44:57 +00:00
|
|
|
final Class<? extends ExpandableBinaryDictionary> dictClass =
|
|
|
|
DICT_TYPE_TO_CLASS.get(dictType);
|
|
|
|
if (dictClass == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
final Method factoryMethod = dictClass.getMethod(DICT_FACTORY_METHOD_NAME,
|
|
|
|
DICT_FACTORY_METHOD_ARG_TYPES);
|
|
|
|
final Object dict = factoryMethod.invoke(null /* obj */,
|
2014-06-10 09:05:10 +00:00
|
|
|
new Object[] { context, locale, dictFile, dictNamePrefix });
|
2014-05-01 09:44:57 +00:00
|
|
|
return (ExpandableBinaryDictionary) dict;
|
|
|
|
} catch (final NoSuchMethodException | SecurityException | IllegalAccessException
|
|
|
|
| IllegalArgumentException | InvocationTargetException e) {
|
|
|
|
Log.e(TAG, "Cannot create dictionary: " + dictType, e);
|
2014-05-01 06:51:03 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-03-25 06:35:20 +00:00
|
|
|
public void resetDictionaries(final Context context, final Locale newLocale,
|
|
|
|
final boolean useContactsDict, final boolean usePersonalizedDicts,
|
|
|
|
final boolean forceReloadMainDictionary,
|
|
|
|
final DictionaryInitializationListener listener) {
|
2014-06-10 09:05:10 +00:00
|
|
|
resetDictionariesWithDictNamePrefix(context, newLocale, useContactsDict,
|
|
|
|
usePersonalizedDicts, forceReloadMainDictionary, listener, "" /* dictNamePrefix */);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void resetDictionariesWithDictNamePrefix(final Context context, final Locale newLocale,
|
|
|
|
final boolean useContactsDict, final boolean usePersonalizedDicts,
|
|
|
|
final boolean forceReloadMainDictionary,
|
|
|
|
final DictionaryInitializationListener listener,
|
|
|
|
final String dictNamePrefix) {
|
2014-03-25 06:35:20 +00:00
|
|
|
final boolean localeHasBeenChanged = !newLocale.equals(mDictionaries.mLocale);
|
|
|
|
// We always try to have the main dictionary. Other dictionaries can be unused.
|
|
|
|
final boolean reloadMainDictionary = localeHasBeenChanged || forceReloadMainDictionary;
|
2014-05-01 09:44:57 +00:00
|
|
|
// TODO: Make subDictTypesToUse configurable by resource or a static final list.
|
2014-05-23 11:18:17 +00:00
|
|
|
final HashSet<String> subDictTypesToUse = new HashSet<>();
|
2014-05-01 06:51:03 +00:00
|
|
|
if (useContactsDict) {
|
2014-05-01 09:44:57 +00:00
|
|
|
subDictTypesToUse.add(Dictionary.TYPE_CONTACTS);
|
2014-05-01 06:51:03 +00:00
|
|
|
}
|
|
|
|
subDictTypesToUse.add(Dictionary.TYPE_USER);
|
|
|
|
if (usePersonalizedDicts) {
|
|
|
|
subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY);
|
|
|
|
subDictTypesToUse.add(Dictionary.TYPE_PERSONALIZATION);
|
2014-04-28 13:01:51 +00:00
|
|
|
subDictTypesToUse.add(Dictionary.TYPE_CONTEXTUAL);
|
2014-05-01 06:51:03 +00:00
|
|
|
}
|
2014-03-25 06:35:20 +00:00
|
|
|
|
|
|
|
final Dictionary newMainDict;
|
|
|
|
if (reloadMainDictionary) {
|
|
|
|
// The main dictionary will be asynchronously loaded.
|
|
|
|
newMainDict = null;
|
|
|
|
} else {
|
2014-04-28 13:09:00 +00:00
|
|
|
newMainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN);
|
2014-03-25 06:35:20 +00:00
|
|
|
}
|
|
|
|
|
2014-05-23 11:18:17 +00:00
|
|
|
final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>();
|
2014-05-01 06:51:03 +00:00
|
|
|
for (final String dictType : SUB_DICT_TYPES) {
|
|
|
|
if (!subDictTypesToUse.contains(dictType)) {
|
|
|
|
// This dictionary will not be used.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
final ExpandableBinaryDictionary dict;
|
|
|
|
if (!localeHasBeenChanged && mDictionaries.hasDict(dictType)) {
|
|
|
|
// Continue to use current dictionary.
|
|
|
|
dict = mDictionaries.getSubDict(dictType);
|
|
|
|
} else {
|
|
|
|
// Start to use new dictionary.
|
2014-06-10 09:05:10 +00:00
|
|
|
dict = getSubDict(dictType, context, newLocale, null /* dictFile */,
|
|
|
|
dictNamePrefix);
|
2014-05-01 06:51:03 +00:00
|
|
|
}
|
|
|
|
subDicts.put(dictType, dict);
|
2014-03-25 06:35:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Replace Dictionaries.
|
2014-05-01 06:51:03 +00:00
|
|
|
final Dictionaries newDictionaries = new Dictionaries(newLocale, newMainDict, subDicts);
|
2014-03-25 06:35:20 +00:00
|
|
|
final Dictionaries oldDictionaries;
|
|
|
|
synchronized (mLock) {
|
|
|
|
oldDictionaries = mDictionaries;
|
|
|
|
mDictionaries = newDictionaries;
|
2014-05-07 07:45:37 +00:00
|
|
|
mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context);
|
2014-03-25 06:35:20 +00:00
|
|
|
if (reloadMainDictionary) {
|
|
|
|
asyncReloadMainDictionary(context, newLocale, listener);
|
|
|
|
}
|
|
|
|
}
|
2014-04-22 23:24:22 +00:00
|
|
|
if (listener != null) {
|
|
|
|
listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary());
|
|
|
|
}
|
2014-03-25 06:35:20 +00:00
|
|
|
// Clean up old dictionaries.
|
2014-04-23 20:13:56 +00:00
|
|
|
if (reloadMainDictionary) {
|
|
|
|
oldDictionaries.closeDict(Dictionary.TYPE_MAIN);
|
2014-03-25 06:35:20 +00:00
|
|
|
}
|
2014-05-01 06:51:03 +00:00
|
|
|
for (final String dictType : SUB_DICT_TYPES) {
|
|
|
|
if (localeHasBeenChanged || !subDictTypesToUse.contains(dictType)) {
|
|
|
|
oldDictionaries.closeDict(dictType);
|
|
|
|
}
|
2014-03-25 06:35:20 +00:00
|
|
|
}
|
2014-04-23 20:13:56 +00:00
|
|
|
oldDictionaries.mSubDictMap.clear();
|
2014-01-08 09:59:43 +00:00
|
|
|
}
|
|
|
|
|
2014-03-25 06:35:20 +00:00
|
|
|
private void asyncReloadMainDictionary(final Context context, final Locale locale,
|
|
|
|
final DictionaryInitializationListener listener) {
|
|
|
|
final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1);
|
|
|
|
mLatchForWaitingLoadingMainDictionary = latchForWaitingLoadingMainDictionary;
|
|
|
|
ExecutorUtils.getExecutor("InitializeBinaryDictionary").execute(new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
final Dictionary mainDict =
|
|
|
|
DictionaryFactory.createMainDictionaryFromManager(context, locale);
|
|
|
|
synchronized (mLock) {
|
|
|
|
if (locale.equals(mDictionaries.mLocale)) {
|
|
|
|
mDictionaries.setMainDict(mainDict);
|
|
|
|
} else {
|
|
|
|
// Dictionary facilitator has been reset for another locale.
|
|
|
|
mainDict.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (listener != null) {
|
2014-04-22 23:24:22 +00:00
|
|
|
listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary());
|
2014-03-25 06:35:20 +00:00
|
|
|
}
|
|
|
|
latchForWaitingLoadingMainDictionary.countDown();
|
|
|
|
}
|
|
|
|
});
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@UsedForTesting
|
2014-03-25 06:35:20 +00:00
|
|
|
public void resetDictionariesForTesting(final Context context, final Locale locale,
|
2014-02-26 10:14:16 +00:00
|
|
|
final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles,
|
|
|
|
final Map<String, Map<String, String>> additionalDictAttributes) {
|
2014-03-25 06:35:20 +00:00
|
|
|
Dictionary mainDictionary = null;
|
2014-05-23 11:18:17 +00:00
|
|
|
final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>();
|
2014-03-25 06:35:20 +00:00
|
|
|
|
2013-12-26 09:26:28 +00:00
|
|
|
for (final String dictType : dictionaryTypes) {
|
|
|
|
if (dictType.equals(Dictionary.TYPE_MAIN)) {
|
2014-03-25 06:35:20 +00:00
|
|
|
mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context, locale);
|
2014-05-01 06:51:03 +00:00
|
|
|
} else {
|
|
|
|
final File dictFile = dictionaryFiles.get(dictType);
|
|
|
|
final ExpandableBinaryDictionary dict = getSubDict(
|
2014-06-10 09:05:10 +00:00
|
|
|
dictType, context, locale, dictFile, "" /* dictNamePrefix */);
|
2014-02-26 10:14:16 +00:00
|
|
|
if (additionalDictAttributes.containsKey(dictType)) {
|
2014-05-01 06:51:03 +00:00
|
|
|
dict.clearAndFlushDictionaryWithAdditionalAttributes(
|
2014-02-26 10:14:16 +00:00
|
|
|
additionalDictAttributes.get(dictType));
|
|
|
|
}
|
2014-05-01 06:51:03 +00:00
|
|
|
if (dict == null) {
|
|
|
|
throw new RuntimeException("Unknown dictionary type: " + dictType);
|
2014-02-26 10:14:16 +00:00
|
|
|
}
|
2014-05-01 06:51:03 +00:00
|
|
|
dict.reloadDictionaryIfRequired();
|
|
|
|
dict.waitAllTasksForTests();
|
|
|
|
subDicts.put(dictType, dict);
|
2013-12-26 09:26:28 +00:00
|
|
|
}
|
|
|
|
}
|
2014-05-01 06:51:03 +00:00
|
|
|
mDictionaries = new Dictionaries(locale, mainDictionary, subDicts);
|
2013-12-27 05:49:32 +00:00
|
|
|
}
|
|
|
|
|
2014-03-25 06:35:20 +00:00
|
|
|
public void closeDictionaries() {
|
|
|
|
final Dictionaries dictionaries;
|
|
|
|
synchronized (mLock) {
|
|
|
|
dictionaries = mDictionaries;
|
|
|
|
mDictionaries = new Dictionaries();
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
2014-06-05 10:05:58 +00:00
|
|
|
for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
|
2014-04-28 13:09:00 +00:00
|
|
|
dictionaries.closeDict(dictType);
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
2014-05-23 07:10:36 +00:00
|
|
|
mDistracterFilter.close();
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// The main dictionary could have been loaded asynchronously. Don't cache the return value
|
|
|
|
// of this method.
|
2014-03-25 06:35:20 +00:00
|
|
|
public boolean hasInitializedMainDictionary() {
|
2014-04-28 13:09:00 +00:00
|
|
|
final Dictionary mainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN);
|
2014-04-25 06:08:48 +00:00
|
|
|
return mainDict != null && mainDict.isInitialized();
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
|
|
|
|
2014-01-16 01:54:17 +00:00
|
|
|
public boolean hasPersonalizationDictionary() {
|
2014-04-23 20:13:56 +00:00
|
|
|
return mDictionaries.hasDict(Dictionary.TYPE_PERSONALIZATION);
|
2014-01-16 01:54:17 +00:00
|
|
|
}
|
|
|
|
|
2014-02-24 05:47:54 +00:00
|
|
|
public void flushPersonalizationDictionary() {
|
2014-04-25 04:37:49 +00:00
|
|
|
final ExpandableBinaryDictionary personalizationDict =
|
|
|
|
mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION);
|
2014-03-25 06:35:20 +00:00
|
|
|
if (personalizationDict != null) {
|
2014-04-24 21:48:01 +00:00
|
|
|
personalizationDict.asyncFlushBinaryDictionary();
|
2014-02-24 05:47:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-01-08 09:59:43 +00:00
|
|
|
public void waitForLoadingMainDictionary(final long timeout, final TimeUnit unit)
|
|
|
|
throws InterruptedException {
|
|
|
|
mLatchForWaitingLoadingMainDictionary.await(timeout, unit);
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
|
|
|
|
2014-02-20 13:20:43 +00:00
|
|
|
@UsedForTesting
|
|
|
|
public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit)
|
|
|
|
throws InterruptedException {
|
|
|
|
waitForLoadingMainDictionary(timeout, unit);
|
2014-04-23 20:13:56 +00:00
|
|
|
final Map<String, ExpandableBinaryDictionary> dictMap = mDictionaries.mSubDictMap;
|
|
|
|
for (final ExpandableBinaryDictionary dict : dictMap.values()) {
|
|
|
|
dict.waitAllTasksForTests();
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isUserDictionaryEnabled() {
|
2014-04-30 11:35:08 +00:00
|
|
|
return mIsUserDictEnabled;
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
|
|
|
|
2014-04-30 11:35:08 +00:00
|
|
|
public void addWordToUserDictionary(final Context context, final String word) {
|
|
|
|
final Locale locale = getLocale();
|
|
|
|
if (locale == null) {
|
2013-12-26 11:28:37 +00:00
|
|
|
return;
|
|
|
|
}
|
2014-04-30 11:35:08 +00:00
|
|
|
UserBinaryDictionary.addWordToUserDictionary(context, locale, word);
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
|
|
|
|
2014-01-14 06:23:58 +00:00
|
|
|
public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized,
|
2014-05-21 02:15:38 +00:00
|
|
|
final PrevWordsInfo prevWordsInfo, final int timeStampInSeconds,
|
2014-05-12 10:48:58 +00:00
|
|
|
final boolean blockPotentiallyOffensive) {
|
2014-03-25 06:35:20 +00:00
|
|
|
final Dictionaries dictionaries = mDictionaries;
|
2014-05-12 09:04:43 +00:00
|
|
|
final String[] words = suggestion.split(Constants.WORD_SEPARATOR);
|
|
|
|
for (int i = 0; i < words.length; i++) {
|
|
|
|
final String currentWord = words[i];
|
2014-05-21 02:15:38 +00:00
|
|
|
final PrevWordsInfo prevWordsInfoForCurrentWord =
|
|
|
|
(i == 0) ? prevWordsInfo : new PrevWordsInfo(words[i - 1]);
|
2014-05-12 09:04:43 +00:00
|
|
|
final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false;
|
2014-05-21 02:15:38 +00:00
|
|
|
addWordToUserHistory(dictionaries, prevWordsInfoForCurrentWord, currentWord,
|
2014-05-12 10:48:58 +00:00
|
|
|
wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive);
|
2014-05-12 09:04:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-05-21 02:15:38 +00:00
|
|
|
private void addWordToUserHistory(final Dictionaries dictionaries,
|
|
|
|
final PrevWordsInfo prevWordsInfo, final String word, final boolean wasAutoCapitalized,
|
|
|
|
final int timeStampInSeconds, final boolean blockPotentiallyOffensive) {
|
2014-04-24 21:48:01 +00:00
|
|
|
final ExpandableBinaryDictionary userHistoryDictionary =
|
|
|
|
dictionaries.getSubDict(Dictionary.TYPE_USER_HISTORY);
|
2014-04-25 06:08:48 +00:00
|
|
|
if (userHistoryDictionary == null) {
|
2013-12-26 13:24:59 +00:00
|
|
|
return;
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
2014-05-27 07:09:59 +00:00
|
|
|
final int maxFreq = getFrequency(word);
|
2014-05-12 10:48:58 +00:00
|
|
|
if (maxFreq == 0 && blockPotentiallyOffensive) {
|
2013-12-26 13:24:59 +00:00
|
|
|
return;
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
2014-05-12 09:04:43 +00:00
|
|
|
final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale);
|
2014-01-28 07:31:37 +00:00
|
|
|
final String secondWord;
|
|
|
|
if (wasAutoCapitalized) {
|
2014-05-12 09:04:43 +00:00
|
|
|
if (isValidWord(word, false /* ignoreCase */)
|
|
|
|
&& !isValidWord(lowerCasedWord, false /* ignoreCase */)) {
|
2014-04-16 12:13:51 +00:00
|
|
|
// If the word was auto-capitalized and exists only as a capitalized word in the
|
|
|
|
// dictionary, then we must not downcase it before registering it. For example,
|
|
|
|
// the name of the contacts in start-of-sentence position would come here with the
|
|
|
|
// wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version
|
|
|
|
// of that contact's name which would end up popping in suggestions.
|
2014-05-12 09:04:43 +00:00
|
|
|
secondWord = word;
|
2014-04-16 12:13:51 +00:00
|
|
|
} else {
|
|
|
|
// If however the word is not in the dictionary, or exists as a lower-case word
|
|
|
|
// only, then we consider that was a lower-case word that had been auto-capitalized.
|
2014-05-12 09:04:43 +00:00
|
|
|
secondWord = lowerCasedWord;
|
2014-04-16 12:13:51 +00:00
|
|
|
}
|
2014-01-28 07:31:37 +00:00
|
|
|
} else {
|
|
|
|
// HACK: We'd like to avoid adding the capitalized form of common words to the User
|
|
|
|
// History dictionary in order to avoid suggesting them until the dictionary
|
|
|
|
// consolidation is done.
|
|
|
|
// TODO: Remove this hack when ready.
|
2014-04-23 20:13:56 +00:00
|
|
|
final int lowerCaseFreqInMainDict = dictionaries.hasDict(Dictionary.TYPE_MAIN) ?
|
2014-05-12 09:04:43 +00:00
|
|
|
dictionaries.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) :
|
2014-01-28 07:31:37 +00:00
|
|
|
Dictionary.NOT_A_PROBABILITY;
|
2014-03-25 06:35:20 +00:00
|
|
|
if (maxFreq < lowerCaseFreqInMainDict
|
|
|
|
&& lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) {
|
2014-01-28 07:31:37 +00:00
|
|
|
// Use lower cased word as the word can be a distracter of the popular word.
|
2014-05-12 09:04:43 +00:00
|
|
|
secondWord = lowerCasedWord;
|
2014-01-28 07:31:37 +00:00
|
|
|
} else {
|
2014-05-12 09:04:43 +00:00
|
|
|
secondWord = word;
|
2014-01-28 07:31:37 +00:00
|
|
|
}
|
|
|
|
}
|
2014-01-14 06:23:58 +00:00
|
|
|
// We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
|
|
|
|
// We don't add words with 0-frequency (assuming they would be profanity etc.).
|
2013-12-26 11:28:37 +00:00
|
|
|
final boolean isValid = maxFreq > 0;
|
2014-05-21 02:15:38 +00:00
|
|
|
UserHistoryDictionary.addToDictionary(userHistoryDictionary, prevWordsInfo, secondWord,
|
2014-05-26 08:28:27 +00:00
|
|
|
isValid, timeStampInSeconds, mDistracterFilter);
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
|
|
|
|
2014-05-21 02:15:38 +00:00
|
|
|
public void cancelAddingUserHistory(final PrevWordsInfo prevWordsInfo,
|
|
|
|
final String committedWord) {
|
2014-04-24 21:48:01 +00:00
|
|
|
final ExpandableBinaryDictionary userHistoryDictionary =
|
|
|
|
mDictionaries.getSubDict(Dictionary.TYPE_USER_HISTORY);
|
2014-03-25 06:35:20 +00:00
|
|
|
if (userHistoryDictionary != null) {
|
2014-05-21 02:15:38 +00:00
|
|
|
userHistoryDictionary.removeNgramDynamically(prevWordsInfo, committedWord);
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Revise the way to fusion suggestion results.
|
2014-03-25 06:35:20 +00:00
|
|
|
public SuggestionResults getSuggestionResults(final WordComposer composer,
|
2014-05-19 04:55:40 +00:00
|
|
|
final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
|
2013-12-26 11:28:37 +00:00
|
|
|
final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
|
2014-03-25 06:35:20 +00:00
|
|
|
final int sessionId, final ArrayList<SuggestedWordInfo> rawSuggestions) {
|
|
|
|
final Dictionaries dictionaries = mDictionaries;
|
|
|
|
final SuggestionResults suggestionResults =
|
|
|
|
new SuggestionResults(dictionaries.mLocale, SuggestedWords.MAX_SUGGESTIONS);
|
2014-04-02 15:47:28 +00:00
|
|
|
final float[] languageWeight = new float[] { Dictionary.NOT_A_LANGUAGE_WEIGHT };
|
2014-06-05 10:05:58 +00:00
|
|
|
for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
|
2014-04-28 13:09:00 +00:00
|
|
|
final Dictionary dictionary = dictionaries.getDict(dictType);
|
2014-01-10 12:42:08 +00:00
|
|
|
if (null == dictionary) continue;
|
2014-02-06 06:51:04 +00:00
|
|
|
final ArrayList<SuggestedWordInfo> dictionarySuggestions =
|
2014-05-19 04:55:40 +00:00
|
|
|
dictionary.getSuggestions(composer, prevWordsInfo, proximityInfo,
|
2014-04-02 15:47:28 +00:00
|
|
|
blockOffensiveWords, additionalFeaturesOptions, sessionId,
|
|
|
|
languageWeight);
|
2014-02-06 06:51:04 +00:00
|
|
|
if (null == dictionarySuggestions) continue;
|
2014-03-25 06:35:20 +00:00
|
|
|
suggestionResults.addAll(dictionarySuggestions);
|
2014-02-06 06:51:04 +00:00
|
|
|
if (null != rawSuggestions) {
|
|
|
|
rawSuggestions.addAll(dictionarySuggestions);
|
|
|
|
}
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
2014-03-25 06:35:20 +00:00
|
|
|
return suggestionResults;
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|
|
|
|
|
2013-12-26 11:47:46 +00:00
|
|
|
public boolean isValidMainDictWord(final String word) {
|
2014-04-28 13:09:00 +00:00
|
|
|
final Dictionary mainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN);
|
|
|
|
if (TextUtils.isEmpty(word) || mainDict == null) {
|
2013-12-26 11:47:46 +00:00
|
|
|
return false;
|
|
|
|
}
|
2014-04-28 13:09:00 +00:00
|
|
|
return mainDict.isValidWord(word);
|
2013-12-26 11:47:46 +00:00
|
|
|
}
|
|
|
|
|
2013-12-26 11:28:37 +00:00
|
|
|
public boolean isValidWord(final String word, final boolean ignoreCase) {
|
|
|
|
if (TextUtils.isEmpty(word)) {
|
|
|
|
return false;
|
|
|
|
}
|
2014-03-25 06:35:20 +00:00
|
|
|
final Dictionaries dictionaries = mDictionaries;
|
2014-03-27 03:03:58 +00:00
|
|
|
if (dictionaries.mLocale == null) {
|
|
|
|
return false;
|
|
|
|
}
|
2014-03-25 06:35:20 +00:00
|
|
|
final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale);
|
2014-06-05 10:05:58 +00:00
|
|
|
for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
|
2014-04-28 13:09:00 +00:00
|
|
|
final Dictionary dictionary = dictionaries.getDict(dictType);
|
2013-12-26 11:28:37 +00:00
|
|
|
// Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
|
|
|
|
// would be immutable once it's finished initializing, but concretely a null test is
|
|
|
|
// probably good enough for the time being.
|
|
|
|
if (null == dictionary) continue;
|
|
|
|
if (dictionary.isValidWord(word)
|
|
|
|
|| (ignoreCase && dictionary.isValidWord(lowerCasedWord))) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2014-06-05 10:05:58 +00:00
|
|
|
private int getFrequencyInternal(final String word,
|
|
|
|
final boolean isGettingMaxFrequencyOfExactMatches) {
|
2013-12-26 11:28:37 +00:00
|
|
|
if (TextUtils.isEmpty(word)) {
|
|
|
|
return Dictionary.NOT_A_PROBABILITY;
|
|
|
|
}
|
2014-06-05 10:05:58 +00:00
|
|
|
int maxFreq = Dictionary.NOT_A_PROBABILITY;
|
2014-04-28 13:09:00 +00:00
|
|
|
final Dictionaries dictionaries = mDictionaries;
|
2014-06-05 10:05:58 +00:00
|
|
|
for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
|
2014-04-28 13:09:00 +00:00
|
|
|
final Dictionary dictionary = dictionaries.getDict(dictType);
|
|
|
|
if (dictionary == null) continue;
|
2014-06-05 10:05:58 +00:00
|
|
|
final int tempFreq;
|
|
|
|
if (isGettingMaxFrequencyOfExactMatches) {
|
|
|
|
tempFreq = dictionary.getMaxFrequencyOfExactMatches(word);
|
|
|
|
} else {
|
|
|
|
tempFreq = dictionary.getFrequency(word);
|
|
|
|
}
|
2013-12-26 11:28:37 +00:00
|
|
|
if (tempFreq >= maxFreq) {
|
|
|
|
maxFreq = tempFreq;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return maxFreq;
|
|
|
|
}
|
|
|
|
|
2014-06-05 10:05:58 +00:00
|
|
|
public int getFrequency(final String word) {
|
|
|
|
return getFrequencyInternal(word, false /* isGettingMaxFrequencyOfExactMatches */);
|
|
|
|
}
|
|
|
|
|
|
|
|
public int getMaxFrequencyOfExactMatches(final String word) {
|
|
|
|
return getFrequencyInternal(word, true /* isGettingMaxFrequencyOfExactMatches */);
|
|
|
|
}
|
|
|
|
|
2014-01-29 03:00:33 +00:00
|
|
|
public void clearUserHistoryDictionary() {
|
2014-04-23 20:13:56 +00:00
|
|
|
final ExpandableBinaryDictionary userHistoryDict =
|
|
|
|
mDictionaries.getSubDict(Dictionary.TYPE_USER_HISTORY);
|
2014-03-25 06:35:20 +00:00
|
|
|
if (userHistoryDict == null) {
|
2014-01-29 03:00:33 +00:00
|
|
|
return;
|
|
|
|
}
|
2014-04-24 18:25:47 +00:00
|
|
|
userHistoryDict.clear();
|
2014-01-29 03:00:33 +00:00
|
|
|
}
|
|
|
|
|
2014-01-16 01:54:17 +00:00
|
|
|
// This method gets called only when the IME receives a notification to remove the
|
|
|
|
// personalization dictionary.
|
|
|
|
public void clearPersonalizationDictionary() {
|
2014-04-23 20:13:56 +00:00
|
|
|
final ExpandableBinaryDictionary personalizationDict =
|
|
|
|
mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION);
|
2014-03-25 06:35:20 +00:00
|
|
|
if (personalizationDict == null) {
|
2014-01-16 01:54:17 +00:00
|
|
|
return;
|
|
|
|
}
|
2014-04-24 18:25:47 +00:00
|
|
|
personalizationDict.clear();
|
2014-01-16 01:54:17 +00:00
|
|
|
}
|
|
|
|
|
2014-05-23 07:10:36 +00:00
|
|
|
public void addEntriesToPersonalizationDictionary(
|
|
|
|
final PersonalizationDataChunk personalizationDataChunk,
|
|
|
|
final SpacingAndPunctuations spacingAndPunctuations,
|
2014-01-16 01:54:17 +00:00
|
|
|
final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) {
|
2014-06-08 22:55:27 +00:00
|
|
|
final ExpandableBinaryDictionary personalizationDict =
|
|
|
|
mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION);
|
2014-06-09 03:26:59 +00:00
|
|
|
if (personalizationDict == null) {
|
|
|
|
if (callback != null) {
|
|
|
|
callback.onFinished();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2014-05-23 07:10:36 +00:00
|
|
|
final ArrayList<LanguageModelParam> languageModelParams =
|
|
|
|
LanguageModelParam.createLanguageModelParamsFrom(
|
|
|
|
personalizationDataChunk.mTokens,
|
|
|
|
personalizationDataChunk.mTimestampInSeconds,
|
|
|
|
this /* dictionaryFacilitator */, spacingAndPunctuations,
|
2014-06-08 22:55:27 +00:00
|
|
|
new DistracterFilterCheckingIsInDictionary(
|
|
|
|
mDistracterFilter, personalizationDict));
|
2014-06-09 03:26:59 +00:00
|
|
|
if (languageModelParams == null || languageModelParams.isEmpty()) {
|
2014-01-16 01:54:17 +00:00
|
|
|
if (callback != null) {
|
|
|
|
callback.onFinished();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2014-04-25 04:37:49 +00:00
|
|
|
personalizationDict.addMultipleDictionaryEntriesDynamically(languageModelParams, callback);
|
2014-01-16 01:54:17 +00:00
|
|
|
}
|
2014-02-04 12:29:09 +00:00
|
|
|
|
|
|
|
public void dumpDictionaryForDebug(final String dictName) {
|
2014-04-23 20:13:56 +00:00
|
|
|
final ExpandableBinaryDictionary dictToDump = mDictionaries.getSubDict(dictName);
|
2014-02-04 12:29:09 +00:00
|
|
|
if (dictToDump == null) {
|
|
|
|
Log.e(TAG, "Cannot dump " + dictName + ". "
|
|
|
|
+ "The dictionary is not being used for suggestion or cannot be dumped.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
dictToDump.dumpAllWordsForDebug();
|
|
|
|
}
|
2013-12-26 11:28:37 +00:00
|
|
|
}
|