Skip to content
Snippets Groups Projects
Commit b85349e9 authored by Keisuke Kuroyanagi's avatar Keisuke Kuroyanagi Committed by Android (Google) Code Review
Browse files

Merge "Create DictionaryFacilitatorLruCache."

parents af5c2b24 d267764d
No related branches found
No related tags found
No related merge requests found
/*
* Copyright (C) 2014 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 java.util.HashSet;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import com.android.inputmethod.annotations.UsedForTesting;
import android.content.Context;
import android.util.Log;
import android.util.LruCache;
/**
* Cache for dictionary facilitators of multiple locales.
* This class automatically creates and releases facilitator instances using LRU policy.
*/
public class DictionaryFacilitatorLruCache {
private static final String TAG = DictionaryFacilitatorLruCache.class.getSimpleName();
private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000;
private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5;
/**
* Class extends LruCache. This class tracks cached locales and closes evicted dictionaries by
* overriding entryRemoved.
*/
private static class DictionaryFacilitatorLruCacheInner extends
LruCache<Locale, DictionaryFacilitator> {
private final HashSet<Locale> mCachedLocales;
public DictionaryFacilitatorLruCacheInner(final HashSet<Locale> cachedLocales,
final int maxSize) {
super(maxSize);
mCachedLocales = cachedLocales;
}
@Override
protected void entryRemoved(boolean evicted, Locale key,
DictionaryFacilitator oldValue, DictionaryFacilitator newValue) {
if (oldValue != null && oldValue != newValue) {
oldValue.closeDictionaries();
}
if (key != null && newValue == null) {
// Remove locale from the cache when the dictionary facilitator for the locale is
// evicted and new facilitator is not set for the locale.
mCachedLocales.remove(key);
if (size() >= maxSize()) {
Log.w(TAG, "DictionaryFacilitator for " + key.toString()
+ " has been evicted due to cache size limit."
+ " size: " + size() + ", maxSize: " + maxSize());
}
}
}
}
private final Context mContext;
private final HashSet<Locale> mCachedLocales = new HashSet<>();
private final String mDictionaryNamePrefix;
private final DictionaryFacilitatorLruCacheInner mLruCache;
private final Object mLock = new Object();
private boolean mUseContactsDictionary = false;
public DictionaryFacilitatorLruCache(final Context context, final int maxSize,
final String dictionaryNamePrefix) {
mContext = context;
mLruCache = new DictionaryFacilitatorLruCacheInner(mCachedLocales, maxSize);
mDictionaryNamePrefix = dictionaryNamePrefix;
}
private void waitForLoadingMainDictionary(final DictionaryFacilitator dictionaryFacilitator) {
for (int i = 0; i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT; i++) {
try {
dictionaryFacilitator.waitForLoadingMainDictionary(
WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
return;
} catch (final InterruptedException e) {
Log.i(TAG, "Interrupted during waiting for loading main dictionary.", e);
if (i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT - 1) {
Log.i(TAG, "Retry", e);
} else {
Log.w(TAG, "Give up retrying. Retried "
+ MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT + " times.", e);
}
}
}
}
private void resetDictionariesForLocaleLocked(final DictionaryFacilitator dictionaryFacilitator,
final Locale locale) {
dictionaryFacilitator.resetDictionariesWithDictNamePrefix(mContext, locale,
mUseContactsDictionary, false /* usePersonalizedDicts */,
false /* forceReloadMainDictionary */, null /* listener */,
mDictionaryNamePrefix);
}
public void setUseContactsDictionary(final boolean useContectsDictionary) {
if (mUseContactsDictionary == useContectsDictionary) {
// The value has not been changed.
return;
}
synchronized (mLock) {
mUseContactsDictionary = useContectsDictionary;
for (final Locale locale : mCachedLocales) {
final DictionaryFacilitator dictionaryFacilitator = mLruCache.get(locale);
resetDictionariesForLocaleLocked(dictionaryFacilitator, locale);
waitForLoadingMainDictionary(dictionaryFacilitator);
}
}
}
public DictionaryFacilitator get(final Locale locale) {
DictionaryFacilitator dictionaryFacilitator = mLruCache.get(locale);
if (dictionaryFacilitator != null) {
// dictionary falicitator for the locale is in the cache.
return dictionaryFacilitator;
}
synchronized (mLock) {
dictionaryFacilitator = mLruCache.get(locale);
if (dictionaryFacilitator != null) {
return dictionaryFacilitator;
}
dictionaryFacilitator = new DictionaryFacilitator();
resetDictionariesForLocaleLocked(dictionaryFacilitator, locale);
waitForLoadingMainDictionary(dictionaryFacilitator);
mLruCache.put(locale, dictionaryFacilitator);
mCachedLocales.add(locale);
return dictionaryFacilitator;
}
}
public void evictAll() {
synchronized (mLock) {
mLruCache.evictAll();
mCachedLocales.clear();
}
}
@UsedForTesting
HashSet<Locale> getCachedLocalesForTesting() {
return mCachedLocales;
}
}
......@@ -16,14 +16,11 @@
package com.android.inputmethod.latin.spellcheck;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.service.textservice.SpellCheckerService;
import android.text.InputType;
import android.util.Log;
import android.util.LruCache;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.view.textservice.SuggestionsInfo;
......@@ -32,40 +29,21 @@ import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardId;
import com.android.inputmethod.keyboard.KeyboardLayoutSet;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.ContactsBinaryDictionary;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.DictionaryCollection;
import com.android.inputmethod.latin.DictionaryFacilitator;
import com.android.inputmethod.latin.DictionaryFactory;
import com.android.inputmethod.latin.DictionaryFacilitatorLruCache;
import com.android.inputmethod.latin.PrevWordsInfo;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.RichInputMethodSubtype;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
import com.android.inputmethod.latin.UserBinaryDictionary;
import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
import com.android.inputmethod.latin.utils.CollectionUtils;
import com.android.inputmethod.latin.utils.LocaleUtils;
import com.android.inputmethod.latin.utils.ScriptUtils;
import com.android.inputmethod.latin.utils.StringUtils;
import com.android.inputmethod.latin.utils.SuggestionResults;
import com.android.inputmethod.latin.WordComposer;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* Service for spell checking, using LatinIME's dictionaries and mechanisms.
......@@ -81,61 +59,28 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 368;
private static final String DICTIONARY_NAME_PREFIX = "spellcheck_";
private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000;
private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5;
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private final HashSet<Locale> mCachedLocales = new HashSet<>();
private final int MAX_NUM_OF_THREADS_READ_DICTIONARY = 2;
private final Semaphore mSemaphore = new Semaphore(MAX_NUM_OF_THREADS_READ_DICTIONARY,
true /* fair */);
// TODO: Make each spell checker session has its own session id.
private final ConcurrentLinkedQueue<Integer> mSessionIdPool = new ConcurrentLinkedQueue<>();
private static class DictionaryFacilitatorLruCache extends
LruCache<Locale, DictionaryFacilitator> {
private final HashSet<Locale> mCachedLocales;
public DictionaryFacilitatorLruCache(final HashSet<Locale> cachedLocales, int maxSize) {
super(maxSize);
mCachedLocales = cachedLocales;
}
@Override
protected void entryRemoved(boolean evicted, Locale key,
DictionaryFacilitator oldValue, DictionaryFacilitator newValue) {
if (oldValue != null && oldValue != newValue) {
oldValue.closeDictionaries();
}
if (key != null && newValue == null) {
// Remove locale from the cache when the dictionary facilitator for the locale is
// evicted and new facilitator is not set for the locale.
mCachedLocales.remove(key);
if (size() >= maxSize()) {
Log.w(TAG, "DictionaryFacilitator for " + key.toString()
+ " has been evicted due to cache size limit."
+ " size: " + size() + ", maxSize: " + maxSize());
}
}
}
}
private static final int MAX_DICTIONARY_FACILITATOR_COUNT = 3;
private final LruCache<Locale, DictionaryFacilitator> mDictionaryFacilitatorCache =
new DictionaryFacilitatorLruCache(mCachedLocales, MAX_DICTIONARY_FACILITATOR_COUNT);
private final DictionaryFacilitatorLruCache mDictionaryFacilitatorCache =
new DictionaryFacilitatorLruCache(this /* context */, MAX_DICTIONARY_FACILITATOR_COUNT,
DICTIONARY_NAME_PREFIX);
private final ConcurrentHashMap<Locale, Keyboard> mKeyboardCache = new ConcurrentHashMap<>();
// The threshold for a suggestion to be considered "recommended".
private float mRecommendedThreshold;
// Whether to use the contacts dictionary
private boolean mUseContactsDictionary;
// TODO: make a spell checker option to block offensive words or not
private final SettingsValuesForSuggestion mSettingsValuesForSuggestion =
new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */,
true /* spaceAwareGestureEnabled */,
null /* additionalFeaturesSettingValues */);
private final Object mDictionaryLock = new Object();
public static final String SINGLE_QUOTE = "\u0027";
public static final String APOSTROPHE = "\u2019";
......@@ -177,20 +122,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
if (!PREF_USE_CONTACTS_KEY.equals(key)) return;
final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true);
if (useContactsDictionary != mUseContactsDictionary) {
mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY);
try {
mUseContactsDictionary = useContactsDictionary;
for (final Locale locale : mCachedLocales) {
final DictionaryFacilitator dictionaryFacilitator =
mDictionaryFacilitatorCache.get(locale);
resetDictionariesForLocale(this /* context */,
dictionaryFacilitator, locale, mUseContactsDictionary);
}
} finally {
mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY);
}
}
mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary);
}
@Override
......@@ -223,7 +155,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
mSemaphore.acquireUninterruptibly();
try {
DictionaryFacilitator dictionaryFacilitatorForLocale =
getDictionaryFacilitatorForLocaleLocked(locale);
mDictionaryFacilitatorCache.get(locale);
return dictionaryFacilitatorForLocale.isValidWord(word, false /* igroreCase */);
} finally {
mSemaphore.release();
......@@ -237,7 +169,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
try {
sessionId = mSessionIdPool.poll();
DictionaryFacilitator dictionaryFacilitatorForLocale =
getDictionaryFacilitatorForLocaleLocked(locale);
mDictionaryFacilitatorCache.get(locale);
return dictionaryFacilitatorForLocale.getSuggestionResults(composer, prevWordsInfo,
proximityInfo, mSettingsValuesForSuggestion, sessionId);
} finally {
......@@ -252,56 +184,18 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
mSemaphore.acquireUninterruptibly();
try {
final DictionaryFacilitator dictionaryFacilitator =
getDictionaryFacilitatorForLocaleLocked(locale);
mDictionaryFacilitatorCache.get(locale);
return dictionaryFacilitator.hasInitializedMainDictionary();
} finally {
mSemaphore.release();
}
}
private DictionaryFacilitator getDictionaryFacilitatorForLocaleLocked(final Locale locale) {
DictionaryFacilitator dictionaryFacilitatorForLocale =
mDictionaryFacilitatorCache.get(locale);
if (dictionaryFacilitatorForLocale == null) {
dictionaryFacilitatorForLocale = new DictionaryFacilitator();
mDictionaryFacilitatorCache.put(locale, dictionaryFacilitatorForLocale);
mCachedLocales.add(locale);
resetDictionariesForLocale(this /* context */, dictionaryFacilitatorForLocale,
locale, mUseContactsDictionary);
}
return dictionaryFacilitatorForLocale;
}
private static void resetDictionariesForLocale(final Context context,
final DictionaryFacilitator dictionaryFacilitator, final Locale locale,
final boolean useContactsDictionary) {
dictionaryFacilitator.resetDictionariesWithDictNamePrefix(context, locale,
useContactsDictionary, false /* usePersonalizedDicts */,
false /* forceReloadMainDictionary */, null /* listener */,
DICTIONARY_NAME_PREFIX);
for (int i = 0; i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT; i++) {
try {
dictionaryFacilitator.waitForLoadingMainDictionary(
WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
return;
} catch (final InterruptedException e) {
Log.i(TAG, "Interrupted during waiting for loading main dictionary.", e);
if (i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT - 1) {
Log.i(TAG, "Retry", e);
} else {
Log.w(TAG, "Give up retrying. Retried "
+ MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT + " times.", e);
}
}
}
}
@Override
public boolean onUnbind(final Intent intent) {
mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY);
try {
mDictionaryFacilitatorCache.evictAll();
mCachedLocales.clear();
} finally {
mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY);
}
......
/*
* Copyright (C) 2014 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 java.util.Locale;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.LargeTest;
@LargeTest
public class DictionaryFacilitatorLruCacheTests extends AndroidTestCase {
static final int MAX_CACHE_SIZE = 2;
static final int MAX_CACHE_SIZE_LARGE = 5;
public void testCacheSize() {
final DictionaryFacilitatorLruCache cache =
new DictionaryFacilitatorLruCache(getContext(), MAX_CACHE_SIZE, "");
assertEquals(0, cache.getCachedLocalesForTesting().size());
assertNotNull(cache.get(Locale.US));
assertEquals(1, cache.getCachedLocalesForTesting().size());
assertNotNull(cache.get(Locale.UK));
assertEquals(2, cache.getCachedLocalesForTesting().size());
assertNotNull(cache.get(Locale.FRENCH));
assertEquals(2, cache.getCachedLocalesForTesting().size());
cache.evictAll();
assertEquals(0, cache.getCachedLocalesForTesting().size());
}
public void testGetFacilitator() {
testGetFacilitator(new DictionaryFacilitatorLruCache(getContext(), MAX_CACHE_SIZE, ""));
testGetFacilitator(new DictionaryFacilitatorLruCache(
getContext(), MAX_CACHE_SIZE_LARGE, ""));
}
private void testGetFacilitator(final DictionaryFacilitatorLruCache cache) {
final DictionaryFacilitator dictionaryFacilitatorEnUs = cache.get(Locale.US);
assertNotNull(dictionaryFacilitatorEnUs);
assertEquals(Locale.US, dictionaryFacilitatorEnUs.getLocale());
final DictionaryFacilitator dictionaryFacilitatorFr = cache.get(Locale.FRENCH);
assertNotNull(dictionaryFacilitatorEnUs);
assertEquals(Locale.FRENCH, dictionaryFacilitatorFr.getLocale());
final DictionaryFacilitator dictionaryFacilitatorDe = cache.get(Locale.GERMANY);
assertNotNull(dictionaryFacilitatorDe);
assertEquals(Locale.GERMANY, dictionaryFacilitatorDe.getLocale());
}
public void testSetUseContactsDictionary() {
testSetUseContactsDictionary(new DictionaryFacilitatorLruCache(
getContext(), MAX_CACHE_SIZE, ""));
testSetUseContactsDictionary(new DictionaryFacilitatorLruCache(
getContext(), MAX_CACHE_SIZE_LARGE, ""));
}
private void testSetUseContactsDictionary(final DictionaryFacilitatorLruCache cache) {
assertNull(cache.get(Locale.US).getSubDictForTesting(Dictionary.TYPE_CONTACTS));
cache.setUseContactsDictionary(true /* useContactsDictionary */);
assertNotNull(cache.get(Locale.US).getSubDictForTesting(Dictionary.TYPE_CONTACTS));
assertNotNull(cache.get(Locale.FRENCH).getSubDictForTesting(Dictionary.TYPE_CONTACTS));
assertNotNull(cache.get(Locale.GERMANY).getSubDictForTesting(Dictionary.TYPE_CONTACTS));
cache.setUseContactsDictionary(false /* useContactsDictionary */);
assertNull(cache.get(Locale.GERMANY).getSubDictForTesting(Dictionary.TYPE_CONTACTS));
assertNull(cache.get(Locale.US).getSubDictForTesting(Dictionary.TYPE_CONTACTS));
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment