Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d267764d authored by Keisuke Kuroyanagi's avatar Keisuke Kuroyanagi
Browse files

Create DictionaryFacilitatorLruCache.

This class is separated from AndroidSpellCheckerService

Bug: 16547557

Change-Id: I3e58521207395588cecb2977234b89067d6da8c3
parent b85bf4eb
Loading
Loading
Loading
Loading
+156 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+8 −114
Original line number Diff line number Diff line
@@ -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);
        }
+81 −0
Original line number Diff line number Diff line
/*
 * 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));
    }
}