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

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

Extract dict operations from Suggest to a new class.

Bug: 8187060
Change-Id: I77775aa50763158d99753c2312fa11fe14267aef
parent 5c45ff12
Loading
Loading
Loading
Loading
+376 −0
Original line number Diff line number Diff line
/*
 * 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;

import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.Suggest.SuggestInitializationListener;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
import com.android.inputmethod.latin.personalization.PersonalizationHelper;
import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
import com.android.inputmethod.latin.settings.SettingsValues;
import com.android.inputmethod.latin.utils.CollectionUtils;

import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

// TODO: Consolidate dictionaries in native code.
public class DictionaryFacilitatorForSuggest {
    public static final String TAG = DictionaryFacilitatorForSuggest.class.getSimpleName();

    private final Context mContext;
    private final Locale mLocale;

    private final ConcurrentHashMap<String, Dictionary> mDictionaries =
            CollectionUtils.newConcurrentHashMap();
    private HashSet<String> mOnlyDictionarySetForDebug = null;

    private Dictionary mMainDictionary;
    private ContactsBinaryDictionary mContactsDictionary;
    private UserBinaryDictionary mUserDictionary;
    private UserHistoryDictionary mUserHistoryDictionary;
    private PersonalizationDictionary mPersonalizationDictionary;

    @UsedForTesting
    private boolean mIsCurrentlyWaitingForMainDictionary = false;

    public DictionaryFacilitatorForSuggest(final Context context, final Locale locale,
            final SettingsValues settingsValues, final SuggestInitializationListener listener) {
        resetMainDict(context, locale, listener);
        mContext = context;
        mLocale = locale;
        // initialize a debug flag for the personalization
        if (settingsValues.mUseOnlyPersonalizationDictionaryForDebug) {
            mOnlyDictionarySetForDebug = new HashSet<String>();
            mOnlyDictionarySetForDebug.add(Dictionary.TYPE_PERSONALIZATION);
        }
        setUserDictionary(new UserBinaryDictionary(context, locale));
    }

    @UsedForTesting
    DictionaryFacilitatorForSuggest(final Context context, final AssetFileAddress[] dictionaryList,
            final Locale locale) {
        final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(dictionaryList,
                false /* useFullEditDistance */, locale);
        mContext = context;
        mLocale = locale;
        mMainDictionary = mainDict;
        addOrReplaceDictionary(Dictionary.TYPE_MAIN, mainDict);
    }

    public void close() {
        final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet();
        dictionaries.addAll(mDictionaries.values());
        for (final Dictionary dictionary : dictionaries) {
            dictionary.close();
        }
        mMainDictionary = null;
        mContactsDictionary = null;
        mUserDictionary = null;
        mUserHistoryDictionary = null;
        mPersonalizationDictionary = null;
    }

    public void resetMainDict(final Context context, final Locale locale,
            final SuggestInitializationListener listener) {
        mIsCurrentlyWaitingForMainDictionary = true;
        mMainDictionary = null;
        if (listener != null) {
            listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
        }
        new Thread("InitializeBinaryDictionary") {
            @Override
            public void run() {
                final DictionaryCollection newMainDict =
                        DictionaryFactory.createMainDictionaryFromManager(context, locale);
                setMainDictionary(newMainDict);
                if (listener != null) {
                    listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
                }
                mIsCurrentlyWaitingForMainDictionary = false;
            }
        }.start();
    }

    // The main dictionary could have been loaded asynchronously.  Don't cache the return value
    // of this method.
    public boolean hasMainDictionary() {
        return null != mMainDictionary && mMainDictionary.isInitialized();
    }

    @UsedForTesting
    public boolean isCurrentlyWaitingForMainDictionary() {
        return mIsCurrentlyWaitingForMainDictionary;
    }

    public Dictionary getMainDictionary() {
        return mMainDictionary;
    }

    private void setMainDictionary(final Dictionary mainDictionary) {
        mMainDictionary = mainDictionary;
        addOrReplaceDictionary(Dictionary.TYPE_MAIN, mainDictionary);
    }

    /**
     * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
     * before the main dictionary, if set. This refers to the system-managed user dictionary.
     */
    @UsedForTesting
    public void setUserDictionary(final UserBinaryDictionary userDictionary) {
        mUserDictionary = userDictionary;
        addOrReplaceDictionary(Dictionary.TYPE_USER, userDictionary);
    }

    /**
     * Sets an optional contacts dictionary resource to be loaded. It is also possible to remove
     * the contacts dictionary by passing null to this method. In this case no contacts dictionary
     * won't be used.
     */
    @UsedForTesting
    public void setContactsDictionary(final ContactsBinaryDictionary contactsDictionary) {
        mContactsDictionary = contactsDictionary;
        addOrReplaceDictionary(Dictionary.TYPE_CONTACTS, contactsDictionary);
    }

    private void setUserHistoryDictionary(final UserHistoryDictionary userHistoryDictionary) {
        mUserHistoryDictionary = userHistoryDictionary;
        addOrReplaceDictionary(Dictionary.TYPE_USER_HISTORY, userHistoryDictionary);
    }

    private void setPersonalizationDictionary(
            final PersonalizationDictionary personalizationDictionary) {
        mPersonalizationDictionary = personalizationDictionary;
        addOrReplaceDictionary(Dictionary.TYPE_PERSONALIZATION, personalizationDictionary);
    }

    /**
     * Set dictionaries that can be turned off according to the user settings.
     *
     * @param oldDictionaryFacilitator the instance having old dictionaries
     * @param settingsValues current SettingsValues
     */
    public void setAdditionalDictionaries(
            final DictionaryFacilitatorForSuggest oldDictionaryFacilitator,
            final SettingsValues settingsValues) {
        // Contacts dictionary
        resetContactsDictionary(null != oldDictionaryFacilitator ?
                oldDictionaryFacilitator.mContactsDictionary : null, settingsValues);
        // User history dictionary & Personalization dictionary
        resetPersonalizedDictionaries(oldDictionaryFacilitator, settingsValues);
    }

    /**
     * Set the user history dictionary and personalization dictionary according to the user
     * settings.
     *
     * @param oldDictionaryFacilitator the instance that has been used
     * @param settingsValues current settingsValues
     */
    // TODO: Consolidate resetPersonalizedDictionaries() and resetContactsDictionary(). Call up the
    // new method for each dictionary.
    private void resetPersonalizedDictionaries(
            final DictionaryFacilitatorForSuggest oldDictionaryFacilitator,
            final SettingsValues settingsValues) {
        final boolean shouldSetDictionaries = settingsValues.mUsePersonalizedDicts;

        final UserHistoryDictionary oldUserHistoryDictionary = (null == oldDictionaryFacilitator) ?
                null : oldDictionaryFacilitator.mUserHistoryDictionary;
        final PersonalizationDictionary oldPersonalizationDictionary =
                (null == oldDictionaryFacilitator) ? null :
                        oldDictionaryFacilitator.mPersonalizationDictionary;
        final UserHistoryDictionary userHistoryDictionaryToUse;
        final PersonalizationDictionary personalizationDictionaryToUse;
        if (!shouldSetDictionaries) {
            userHistoryDictionaryToUse = null;
            personalizationDictionaryToUse = null;
        } else {
            if (null != oldUserHistoryDictionary
                    && oldUserHistoryDictionary.mLocale.equals(mLocale)) {
                userHistoryDictionaryToUse = oldUserHistoryDictionary;
            } else {
                userHistoryDictionaryToUse =
                        PersonalizationHelper.getUserHistoryDictionary(mContext, mLocale);
            }
            if (null != oldPersonalizationDictionary
                    && oldPersonalizationDictionary.mLocale.equals(mLocale)) {
                personalizationDictionaryToUse = oldPersonalizationDictionary;
            } else {
                personalizationDictionaryToUse =
                        PersonalizationHelper.getPersonalizationDictionary(mContext, mLocale);
            }
        }
        setUserHistoryDictionary(userHistoryDictionaryToUse);
        setPersonalizationDictionary(personalizationDictionaryToUse);
    }

    /**
     * Set the contacts dictionary according to the user settings.
     *
     * This method takes an optional contacts dictionary to use when the locale hasn't changed
     * since the contacts dictionary can be opened or closed as necessary depending on the settings.
     *
     * @param oldContactsDictionary an optional dictionary to use, or null
     * @param settingsValues current settingsValues
     */
    private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary,
            final SettingsValues settingsValues) {
        final boolean shouldSetDictionary = settingsValues.mUseContactsDict;
        final ContactsBinaryDictionary dictionaryToUse;
        if (!shouldSetDictionary) {
            // Make sure the dictionary is closed. If it is already closed, this is a no-op,
            // so it's safe to call it anyways.
            if (null != oldContactsDictionary) oldContactsDictionary.close();
            dictionaryToUse = null;
        } else {
            if (null != oldContactsDictionary) {
                if (!oldContactsDictionary.mLocale.equals(mLocale)) {
                    // If the locale has changed then recreate the contacts dictionary. This
                    // allows locale dependent rules for handling bigram name predictions.
                    oldContactsDictionary.close();
                    dictionaryToUse = new ContactsBinaryDictionary(mContext, mLocale);
                } else {
                    // Make sure the old contacts dictionary is opened. If it is already open,
                    // this is a no-op, so it's safe to call it anyways.
                    oldContactsDictionary.reopen(mContext);
                    dictionaryToUse = oldContactsDictionary;
                }
            } else {
                dictionaryToUse = new ContactsBinaryDictionary(mContext, mLocale);
            }
        }
        setContactsDictionary(dictionaryToUse);
    }

    public boolean isUserDictionaryEnabled() {
        if (mUserDictionary == null) {
            return false;
        }
        return mUserDictionary.mEnabled;
    }

    public void addWordToUserDictionary(String word) {
        if (mUserDictionary == null) {
            return;
        }
        mUserDictionary.addWordToUserDictionary(word);
    }

    public String addToUserHistory(final WordComposer wordComposer, final String previousWord,
            final String suggestion) {
        if (mUserHistoryDictionary == null) {
            return null;
        }
        final String secondWord;
        if (wordComposer.wasAutoCapitalized() && !wordComposer.isMostlyCaps()) {
            secondWord = suggestion.toLowerCase(mLocale);
        } else {
            secondWord = suggestion;
        }
        // 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.).
        final int maxFreq = getMaxFrequency(suggestion);
        if (maxFreq == 0) {
            return null;
        }
        final boolean isValid = maxFreq > 0;
        final int timeStamp = (int)TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis()));
        mUserHistoryDictionary.addToDictionary(previousWord, secondWord, isValid, timeStamp);
        return previousWord;
    }

    public void cancelAddingUserHistory(final String previousWord, final String committedWord) {
        if (mUserHistoryDictionary != null) {
            mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
        }
    }

    // TODO: Revise the way to fusion suggestion results.
    public void getSuggestions(final WordComposer composer,
            final String prevWord, final ProximityInfo proximityInfo,
            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
            final int sessionId, final Set<SuggestedWordInfo> suggestionSet) {
        for (final String key : mDictionaries.keySet()) {
            final Dictionary dictionary = mDictionaries.get(key);
            suggestionSet.addAll(dictionary.getSuggestionsWithSessionId(composer, prevWord,
                    proximityInfo, blockOffensiveWords, additionalFeaturesOptions, sessionId));
        }
    }

    public boolean isValidWord(final String word, final boolean ignoreCase) {
        if (TextUtils.isEmpty(word)) {
            return false;
        }
        final String lowerCasedWord = word.toLowerCase(mLocale);
        for (final String key : mDictionaries.keySet()) {
            final Dictionary dictionary = mDictionaries.get(key);
            // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
            // managing to get null in here. Presumably the language is changing to a language with
            // no main dictionary and the monkey manages to type a whole word before the thread
            // that reads the dictionary is started or something?
            // 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;
    }

    private int getMaxFrequency(final String word) {
        if (TextUtils.isEmpty(word)) {
            return Dictionary.NOT_A_PROBABILITY;
        }
        int maxFreq = -1;
        for (final String key : mDictionaries.keySet()) {
            final Dictionary dictionary = mDictionaries.get(key);
            if (null == dictionary) continue;
            final int tempFreq = dictionary.getFrequency(word);
            if (tempFreq >= maxFreq) {
                maxFreq = tempFreq;
            }
        }
        return maxFreq;
    }

    private void addOrReplaceDictionary(final String key, final Dictionary dict) {
        if (mOnlyDictionarySetForDebug != null && !mOnlyDictionarySetForDebug.contains(key)) {
            Log.w(TAG, "Ignore add " + key + " dictionary for debug.");
            return;
        }
        final Dictionary oldDict;
        if (dict == null) {
            oldDict = mDictionaries.remove(key);
        } else {
            oldDict = mDictionaries.put(key, dict);
        }
        if (oldDict != null && dict != oldDict) {
            oldDict.close();
        }
    }
}
+17 −12
Original line number Diff line number Diff line
@@ -472,7 +472,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
        initSuggest();

        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
            ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mInputLogic.mSuggest);
            ResearchLogger.getInstance().init(this, mKeyboardSwitcher);
        }

        // Register to receive ringer mode change and network state change.
@@ -516,7 +516,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
        if (!mHandler.hasPendingReopenDictionaries() && mInputLogic.mSuggest != null) {
            // May need to reset dictionaries depending on the user settings.
            // TODO: Quit setting dictionaries from LatinIME.
            mInputLogic.mSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */,
            mInputLogic.mSuggest.mDictionaryFacilitator.setAdditionalDictionaries(
                    mInputLogic.mSuggest.mDictionaryFacilitator /* oldDictionaryFacilitator */,
                    currentSettingsValues);
        }
        if (currentSettingsValues.mUsePersonalizedDicts) {
@@ -561,10 +562,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
        }

        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
            ResearchLogger.getInstance().initSuggest(newSuggest);
            ResearchLogger.getInstance().initDictionary(newSuggest.mDictionaryFacilitator);
        }
        // TODO: Quit setting dictionaries from LatinIME.
        newSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */, settingsValues);
        newSuggest.mDictionaryFacilitator.setAdditionalDictionaries(
                (mInputLogic.mSuggest == null) ? null : mInputLogic.mSuggest.mDictionaryFacilitator
                        /* oldDictionaryFacilitator */, settingsValues);
        final Suggest oldSuggest = mInputLogic.mSuggest;
        mInputLogic.mSuggest = newSuggest;
        if (oldSuggest != null) oldSuggest.close();
@@ -572,7 +575,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen

    /* package private */ void resetSuggestMainDict() {
        final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
        mInputLogic.mSuggest.resetMainDict(this, subtypeLocale,
        mInputLogic.mSuggest.mDictionaryFacilitator.resetMainDict(this, subtypeLocale,
                this /* SuggestInitializationListener */);
    }

@@ -822,7 +825,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
        mHandler.cancelDoubleSpacePeriodTimer();

        mainKeyboardView.setMainDictionaryAvailability(null != suggest
                ? suggest.hasMainDictionary() : false);
                ? suggest.mDictionaryFacilitator.hasMainDictionary() : false);
        mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn,
                currentSettingsValues.mKeyPreviewPopupDismissDelay);
        mainKeyboardView.setSlidingKeyInputPreviewEnabled(
@@ -1203,7 +1206,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
        } else {
            wordToEdit = word;
        }
        mInputLogic.mSuggest.addWordToUserDictionary(wordToEdit);
        mInputLogic.mSuggest.mDictionaryFacilitator.addWordToUserDictionary(wordToEdit);
    }

    public void displaySettingsDialog() {
@@ -1724,13 +1727,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
                        || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
                        && suggest != null
                        // If the suggestion is not in the dictionary, the hint should be shown.
                        && !suggest.isValidWord(suggestion, true);
                        && !suggest.mDictionaryFacilitator.isValidWord(suggestion,
                                true /* ignoreCase */);

        if (currentSettings.mIsInternal) {
            LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE,
                    Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
        }
        if (showingAddToDictionaryHint && suggest.isUserDictionaryEnabled()) {
        if (showingAddToDictionaryHint
                && suggest.mDictionaryFacilitator.isUserDictionaryEnabled()) {
            mSuggestionStripView.showAddToDictionaryHint(
                    suggestion, currentSettings.mHintToSaveText);
        } else {
@@ -1963,13 +1968,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
    // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
    @UsedForTesting
    /* package for test */ boolean isCurrentlyWaitingForMainDictionary() {
        return mInputLogic.mSuggest.isCurrentlyWaitingForMainDictionary();
        return mInputLogic.mSuggest.mDictionaryFacilitator.isCurrentlyWaitingForMainDictionary();
    }

    // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
    @UsedForTesting
    /* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
        mInputLogic.mSuggest.resetMainDict(this, locale, null);
        mInputLogic.mSuggest.mDictionaryFacilitator.resetMainDict(this, locale, null);
    }

    public void debugDumpStateAndCrashWithException(final String context) {
+14 −329

File changed.

Preview size limit exceeded, changes collapsed.

+3 −2
Original line number Diff line number Diff line
@@ -986,7 +986,7 @@ public final class InputLogic {
        if (suggest == null) return null;

        final String prevWord = mConnection.getNthPreviousWord(settingsValues, 2);
        return suggest.addToUserHistory(mWordComposer, prevWord, suggestion);
        return suggest.mDictionaryFacilitator.addToUserHistory(mWordComposer, prevWord, suggestion);
    }

    public void performUpdateSuggestionStripSync(final SettingsValues settingsValues,
@@ -1190,7 +1190,8 @@ public final class InputLogic {
        mConnection.deleteSurroundingText(deleteLength, 0);
        if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
            if (mSuggest != null) {
                mSuggest.cancelAddingUserHistory(previousWord, committedWord);
                mSuggest.mDictionaryFacilitator.cancelAddingUserHistory(
                        previousWord, committedWord);
            }
        }
        final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
+8 −8
Original line number Diff line number Diff line
@@ -20,7 +20,7 @@ import android.util.Log;

import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.Suggest;
import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest;
import com.android.inputmethod.latin.define.ProductionFlag;

import java.io.IOException;
@@ -75,9 +75,7 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
    // The size of the n-grams logged.  E.g. N_GRAM_SIZE = 2 means to sample bigrams.
    public static final int N_GRAM_SIZE = 2;

    // TODO: Remove dependence on Suggest, and pass in Dictionary as a parameter to an appropriate
    // method.
    private final Suggest mSuggest;
    private final DictionaryFacilitatorForSuggest mDictionaryFacilitator;
    @UsedForTesting
    private Dictionary mDictionaryForTesting;
    private boolean mIsStopping = false;
@@ -89,11 +87,11 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
    /* package for test */ int mNumWordsUntilSafeToSample;

    public MainLogBuffer(final int wordsBetweenSamples, final int numInitialWordsToIgnore,
            final Suggest suggest) {
            final DictionaryFacilitatorForSuggest dictionaryFacilitator) {
        super(N_GRAM_SIZE + wordsBetweenSamples);
        mNumWordsBetweenNGrams = wordsBetweenSamples;
        mNumWordsUntilSafeToSample = DEBUG ? 0 : numInitialWordsToIgnore;
        mSuggest = suggest;
        mDictionaryFacilitator = dictionaryFacilitator;
    }

    @UsedForTesting
@@ -105,8 +103,10 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
        if (mDictionaryForTesting != null) {
            return mDictionaryForTesting;
        }
        if (mSuggest == null || !mSuggest.hasMainDictionary()) return null;
        return mSuggest.getMainDictionary();
        if (mDictionaryFacilitator == null || !mDictionaryFacilitator.hasMainDictionary()) {
            return null;
        }
        return mDictionaryFacilitator.getMainDictionary();
    }

    public void setIsStopping() {
Loading