diff options
Diffstat (limited to 'java/src')
26 files changed, 1159 insertions, 1290 deletions
diff --git a/java/src/com/android/inputmethod/event/CombinerChain.java b/java/src/com/android/inputmethod/event/CombinerChain.java index 5858faa09..d77ece8e6 100644 --- a/java/src/com/android/inputmethod/event/CombinerChain.java +++ b/java/src/com/android/inputmethod/event/CombinerChain.java @@ -22,7 +22,6 @@ import android.text.TextUtils; import com.android.inputmethod.latin.common.Constants; import java.util.ArrayList; -import java.util.HashMap; import javax.annotation.Nonnull; @@ -45,13 +44,6 @@ public class CombinerChain { private SpannableStringBuilder mStateFeedback; private final ArrayList<Combiner> mCombiners; - private static final HashMap<String, Class<? extends Combiner>> IMPLEMENTED_COMBINERS = - new HashMap<>(); - static { - IMPLEMENTED_COMBINERS.put("MyanmarReordering", MyanmarReordering.class); - } - private static final String COMBINER_SPEC_SEPARATOR = ";"; - /** * Create an combiner chain. * @@ -61,15 +53,11 @@ public class CombinerChain { * cursor: we'll start after this. * * @param initialText The text that has already been combined so far. - * @param combinerList A list of combiners to be applied in order. */ - public CombinerChain(final String initialText, final Combiner... combinerList) { + public CombinerChain(final String initialText) { mCombiners = new ArrayList<>(); // The dead key combiner is always active, and always first mCombiners.add(new DeadKeyCombiner()); - for (final Combiner combiner : combinerList) { - mCombiners.add(combiner); - } mCombinedText = new StringBuilder(initialText); mStateFeedback = new SpannableStringBuilder(); } @@ -146,30 +134,4 @@ public class CombinerChain { final SpannableStringBuilder s = new SpannableStringBuilder(mCombinedText); return s.append(mStateFeedback); } - - public static Combiner[] createCombiners(final String spec) { - if (TextUtils.isEmpty(spec)) { - return new Combiner[0]; - } - final String[] combinerDescriptors = spec.split(COMBINER_SPEC_SEPARATOR); - final Combiner[] combiners = new Combiner[combinerDescriptors.length]; - int i = 0; - for (final String combinerDescriptor : combinerDescriptors) { - final Class<? extends Combiner> combinerClass = - IMPLEMENTED_COMBINERS.get(combinerDescriptor); - if (null == combinerClass) { - throw new RuntimeException("Unknown combiner descriptor: " + combinerDescriptor); - } - try { - combiners[i++] = combinerClass.newInstance(); - } catch (InstantiationException e) { - throw new RuntimeException("Unable to instantiate combiner: " + combinerDescriptor, - e); - } catch (IllegalAccessException e) { - throw new RuntimeException("Unable to instantiate combiner: " + combinerDescriptor, - e); - } - } - return combiners; - } } diff --git a/java/src/com/android/inputmethod/event/MyanmarReordering.java b/java/src/com/android/inputmethod/event/MyanmarReordering.java deleted file mode 100644 index 7bc1630f5..000000000 --- a/java/src/com/android/inputmethod/event/MyanmarReordering.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * 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.event; - -import com.android.inputmethod.latin.common.Constants; - -import java.util.ArrayList; -import java.util.Arrays; - -import javax.annotation.Nonnull; - -/** - * A combiner that reorders input for Myanmar. - */ -public class MyanmarReordering implements Combiner { - // U+1031 MYANMAR VOWEL SIGN E - private final static int VOWEL_E = 0x1031; // Code point for vowel E that we need to reorder - // U+200C ZERO WIDTH NON-JOINER - // U+200B ZERO WIDTH SPACE - private final static int ZERO_WIDTH_NON_JOINER = 0x200B; // should be 0x200C - - private final ArrayList<Event> mCurrentEvents = new ArrayList<>(); - - // List of consonants : - // U+1000 MYANMAR LETTER KA - // U+1001 MYANMAR LETTER KHA - // U+1002 MYANMAR LETTER GA - // U+1003 MYANMAR LETTER GHA - // U+1004 MYANMAR LETTER NGA - // U+1005 MYANMAR LETTER CA - // U+1006 MYANMAR LETTER CHA - // U+1007 MYANMAR LETTER JA - // U+1008 MYANMAR LETTER JHA - // U+1009 MYANMAR LETTER NYA - // U+100A MYANMAR LETTER NNYA - // U+100B MYANMAR LETTER TTA - // U+100C MYANMAR LETTER TTHA - // U+100D MYANMAR LETTER DDA - // U+100E MYANMAR LETTER DDHA - // U+100F MYANMAR LETTER NNA - // U+1010 MYANMAR LETTER TA - // U+1011 MYANMAR LETTER THA - // U+1012 MYANMAR LETTER DA - // U+1013 MYANMAR LETTER DHA - // U+1014 MYANMAR LETTER NA - // U+1015 MYANMAR LETTER PA - // U+1016 MYANMAR LETTER PHA - // U+1017 MYANMAR LETTER BA - // U+1018 MYANMAR LETTER BHA - // U+1019 MYANMAR LETTER MA - // U+101A MYANMAR LETTER YA - // U+101B MYANMAR LETTER RA - // U+101C MYANMAR LETTER LA - // U+101D MYANMAR LETTER WA - // U+101E MYANMAR LETTER SA - // U+101F MYANMAR LETTER HA - // U+1020 MYANMAR LETTER LLA - // U+103F MYANMAR LETTER GREAT SA - private static boolean isConsonant(final int codePoint) { - return (codePoint >= 0x1000 && codePoint <= 0x1020) || 0x103F == codePoint; - } - - // List of medials : - // U+103B MYANMAR CONSONANT SIGN MEDIAL YA - // U+103C MYANMAR CONSONANT SIGN MEDIAL RA - // U+103D MYANMAR CONSONANT SIGN MEDIAL WA - // U+103E MYANMAR CONSONANT SIGN MEDIAL HA - // U+105E MYANMAR CONSONANT SIGN MON MEDIAL NA - // U+105F MYANMAR CONSONANT SIGN MON MEDIAL MA - // U+1060 MYANMAR CONSONANT SIGN MON MEDIAL LA - // U+1082 MYANMAR CONSONANT SIGN SHAN MEDIAL WA - private static int[] MEDIAL_LIST = { 0x103B, 0x103C, 0x103D, 0x103E, - 0x105E, 0x105F, 0x1060, 0x1082}; - private static boolean isMedial(final int codePoint) { - return Arrays.binarySearch(MEDIAL_LIST, codePoint) >= 0; - } - - private static boolean isConsonantOrMedial(final int codePoint) { - return isConsonant(codePoint) || isMedial(codePoint); - } - - private Event getLastEvent() { - final int size = mCurrentEvents.size(); - if (size <= 0) { - return null; - } - return mCurrentEvents.get(size - 1); - } - - private CharSequence getCharSequence() { - final StringBuilder s = new StringBuilder(); - for (final Event e : mCurrentEvents) { - s.appendCodePoint(e.mCodePoint); - } - return s; - } - - /** - * Clears the currently combining stream of events and returns the resulting software text - * event corresponding to the stream. Optionally adds a new event to the cleared stream. - * @param newEvent the new event to add to the stream. null if none. - * @return the resulting software text event. Never null. - */ - @Nonnull - private Event clearAndGetResultingEvent(final Event newEvent) { - final CharSequence combinedText; - if (mCurrentEvents.size() > 0) { - combinedText = getCharSequence(); - mCurrentEvents.clear(); - } else { - combinedText = null; - } - if (null != newEvent) { - mCurrentEvents.add(newEvent); - } - return null == combinedText ? Event.createConsumedEvent(newEvent) - : Event.createSoftwareTextEvent(combinedText, Event.NOT_A_KEY_CODE); - } - - @Override - @Nonnull - public Event processEvent(ArrayList<Event> previousEvents, Event newEvent) { - final int codePoint = newEvent.mCodePoint; - if (VOWEL_E == codePoint) { - final Event lastEvent = getLastEvent(); - if (null == lastEvent) { - mCurrentEvents.add(newEvent); - return Event.createConsumedEvent(newEvent); - } - if (isConsonantOrMedial(lastEvent.mCodePoint)) { - final Event resultingEvent = clearAndGetResultingEvent(null); - mCurrentEvents.add(Event.createSoftwareKeypressEvent(ZERO_WIDTH_NON_JOINER, - Event.NOT_A_KEY_CODE, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, - false /* isKeyRepeat */)); - mCurrentEvents.add(newEvent); - return resultingEvent; - } - // VOWEL_E == lastCodePoint. But if that was anything else this is correct too. - return clearAndGetResultingEvent(newEvent); - } - if (isConsonant(codePoint)) { - final Event lastEvent = getLastEvent(); - if (null == lastEvent) { - mCurrentEvents.add(newEvent); - return Event.createConsumedEvent(newEvent); - } - if (VOWEL_E == lastEvent.mCodePoint) { - final int eventSize = mCurrentEvents.size(); - if (eventSize >= 2 - && mCurrentEvents.get(eventSize - 2).mCodePoint == ZERO_WIDTH_NON_JOINER) { - // We have a ZWJN before a vowel E. We need to remove the ZWNJ and then - // reorder the vowel with respect to the consonant. - mCurrentEvents.remove(eventSize - 1); - mCurrentEvents.remove(eventSize - 2); - mCurrentEvents.add(newEvent); - mCurrentEvents.add(lastEvent); - return Event.createConsumedEvent(newEvent); - } - // If there is already a consonant, then we are starting a new syllable. - for (int i = eventSize - 2; i >= 0; --i) { - if (isConsonant(mCurrentEvents.get(i).mCodePoint)) { - return clearAndGetResultingEvent(newEvent); - } - } - // If we come here, we didn't have a consonant so we reorder - mCurrentEvents.remove(eventSize - 1); - mCurrentEvents.add(newEvent); - mCurrentEvents.add(lastEvent); - return Event.createConsumedEvent(newEvent); - } - // lastCodePoint is a consonant/medial. But if it's something else it's fine - return clearAndGetResultingEvent(newEvent); - } - if (isMedial(codePoint)) { - final Event lastEvent = getLastEvent(); - if (null == lastEvent) { - mCurrentEvents.add(newEvent); - return Event.createConsumedEvent(newEvent); - } - if (VOWEL_E == lastEvent.mCodePoint) { - final int eventSize = mCurrentEvents.size(); - // If there is already a consonant, then we are in the middle of a syllable, and we - // need to reorder. - boolean hasConsonant = false; - for (int i = eventSize - 2; i >= 0; --i) { - if (isConsonant(mCurrentEvents.get(i).mCodePoint)) { - hasConsonant = true; - break; - } - } - if (hasConsonant) { - mCurrentEvents.remove(eventSize - 1); - mCurrentEvents.add(newEvent); - mCurrentEvents.add(lastEvent); - return Event.createConsumedEvent(newEvent); - } - // Otherwise, we just commit everything. - return clearAndGetResultingEvent(null); - } - // lastCodePoint is a consonant/medial. But if it's something else it's fine - return clearAndGetResultingEvent(newEvent); - } - final Event lastEvent = getLastEvent(); - if (Constants.CODE_DELETE == newEvent.mKeyCode && null != lastEvent) { - final int eventSize = mCurrentEvents.size(); - if (VOWEL_E == lastEvent.mCodePoint) { - // We have a VOWEL_E at the end. There are four cases. - // - The vowel is the only code point in the buffer. Remove it. - // - The vowel is preceded by a ZWNJ. Remove both vowel E and ZWNJ. - // - The vowel is preceded by a consonant/medial, remove the consonant/medial. - // - In all other cases, it's strange, so just remove the last code point. - if (eventSize <= 1) { - mCurrentEvents.clear(); - } else { // eventSize >= 2 - final int previousCodePoint = mCurrentEvents.get(eventSize - 2).mCodePoint; - if (previousCodePoint == ZERO_WIDTH_NON_JOINER) { - mCurrentEvents.remove(eventSize - 1); - mCurrentEvents.remove(eventSize - 2); - } else if (isConsonantOrMedial(previousCodePoint)) { - mCurrentEvents.remove(eventSize - 2); - } else { - mCurrentEvents.remove(eventSize - 1); - } - } - return Event.createConsumedEvent(newEvent); - } else if (eventSize > 0) { - mCurrentEvents.remove(eventSize - 1); - return Event.createConsumedEvent(newEvent); - } - } - // This character is not part of the combining scheme, so we should reset everything. - if (mCurrentEvents.size() > 0) { - // If we have events in flight, then add the new event and return the resulting event. - mCurrentEvents.add(newEvent); - return clearAndGetResultingEvent(null); - } - // If we don't have any events in flight, then just pass this one through. - return newEvent; - } - - @Override - public CharSequence getCombiningStateFeedback() { - return getCharSequence(); - } - - @Override - public void reset() { - mCurrentEvents.clear(); - } -} diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java index 2055a59bb..7318d4738 100644 --- a/java/src/com/android/inputmethod/keyboard/Keyboard.java +++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java @@ -95,6 +95,9 @@ public class Keyboard { @Nonnull private final ProximityInfo mProximityInfo; + @Nonnull + private final KeyboardLayout mKeyboardLayout; + private final boolean mProximityCharsCorrectionEnabled; public Keyboard(@Nonnull final KeyboardParams params) { @@ -121,6 +124,8 @@ public class Keyboard { mOccupiedWidth, mOccupiedHeight, mMostCommonKeyWidth, mMostCommonKeyHeight, mSortedKeys, params.mTouchPositionCorrection); mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled; + mKeyboardLayout = KeyboardLayout.newKeyboardLayout(mSortedKeys, mMostCommonKeyWidth, + mMostCommonKeyHeight, mOccupiedWidth, mOccupiedHeight); } protected Keyboard(@Nonnull final Keyboard keyboard) { @@ -145,6 +150,7 @@ public class Keyboard { mProximityInfo = keyboard.mProximityInfo; mProximityCharsCorrectionEnabled = keyboard.mProximityCharsCorrectionEnabled; + mKeyboardLayout = keyboard.mKeyboardLayout; } public boolean hasProximityCharsCorrection(final int code) { @@ -164,6 +170,11 @@ public class Keyboard { return mProximityInfo; } + @Nonnull + public KeyboardLayout getKeyboardLayout() { + return mKeyboardLayout; + } + /** * Return the sorted list of keys of this keyboard. * The keys are sorted from top-left to bottom-right order. diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayout.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayout.java new file mode 100644 index 000000000..d0f32078e --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayout.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2015 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.keyboard; + +import com.android.inputmethod.annotations.UsedForTesting; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nonnull; + +/** + * KeyboardLayout maintains the keyboard layout information. + */ +public class KeyboardLayout { + + private final int[] mKeyCodes; + + private final int[] mKeyXCoordinates; + private final int[] mKeyYCoordinates; + + private final int[] mKeyWidths; + private final int[] mKeyHeights; + + public final int mMostCommonKeyWidth; + public final int mMostCommonKeyHeight; + + public final int mKeyboardWidth; + public final int mKeyboardHeight; + + public KeyboardLayout(ArrayList<Key> layoutKeys, int mostCommonKeyWidth, + int mostCommonKeyHeight, int keyboardWidth, int keyboardHeight) { + mMostCommonKeyWidth = mostCommonKeyWidth; + mMostCommonKeyHeight = mostCommonKeyHeight; + mKeyboardWidth = keyboardWidth; + mKeyboardHeight = keyboardHeight; + + mKeyCodes = new int[layoutKeys.size()]; + mKeyXCoordinates = new int[layoutKeys.size()]; + mKeyYCoordinates = new int[layoutKeys.size()]; + mKeyWidths = new int[layoutKeys.size()]; + mKeyHeights = new int[layoutKeys.size()]; + + for (int i = 0; i < layoutKeys.size(); i++) { + Key key = layoutKeys.get(i); + mKeyCodes[i] = Character.toLowerCase(key.getCode()); + mKeyXCoordinates[i] = key.getX(); + mKeyYCoordinates[i] = key.getY(); + mKeyWidths[i] = key.getWidth(); + mKeyHeights[i] = key.getHeight(); + } + } + + @UsedForTesting + public int[] getKeyCodes() { + return mKeyCodes; + } + + /** + * The x-coordinate for the top-left corner of the keys. + * + */ + public int[] getKeyXCoordinates() { + return mKeyXCoordinates; + } + + /** + * The y-coordinate for the top-left corner of the keys. + */ + public int[] getKeyYCoordinates() { + return mKeyYCoordinates; + } + + /** + * The widths of the keys which are smaller than the true hit-area due to the gaps + * between keys. The mostCommonKey(Width/Height) represents the true key width/height + * including the gaps. + */ + public int[] getKeyWidths() { + return mKeyWidths; + } + + /** + * The heights of the keys which are smaller than the true hit-area due to the gaps + * between keys. The mostCommonKey(Width/Height) represents the true key width/height + * including the gaps. + */ + public int[] getKeyHeights() { + return mKeyHeights; + } + + /** + * Factory method to create {@link KeyboardLayout} objects. + */ + public static KeyboardLayout newKeyboardLayout(@Nonnull final List<Key> sortedKeys, + int mostCommonKeyWidth, int mostCommonKeyHeight, + int occupiedWidth, int occupiedHeight) { + final ArrayList<Key> layoutKeys = new ArrayList<Key>(); + for (final Key key : sortedKeys) { + if (!ProximityInfo.needsProximityInfo(key)) { + continue; + } + if (key.getCode() != ',') { + layoutKeys.add(key); + } + } + return new KeyboardLayout(layoutKeys, mostCommonKeyWidth, + mostCommonKeyHeight, occupiedWidth, occupiedHeight); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java index 228b964ea..b9a5eaefb 100644 --- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java +++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java @@ -95,7 +95,7 @@ public class ProximityInfo { private static native void releaseProximityInfoNative(long nativeProximityInfo); - private static boolean needsProximityInfo(final Key key) { + static boolean needsProximityInfo(final Key key) { // Don't include special keys into ProximityInfo. return key.getCode() >= Constants.CODE_SPACE; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java index a81d7ea2a..b50c0a86a 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java @@ -2781,41 +2781,6 @@ public final class KeyboardTextsTable { /* additional_morekeys_symbols_0 */ "0", }; - /* Locale my_MM: Burmese (Myanmar) */ - private static final String[] TEXTS_my_MM = { - /* morekeys_a ~ */ - null, null, null, null, - /* ~ morekeys_u */ - // Label for "switch to alphabetic" key. - // U+1000: "က" MYANMAR LETTER KA - // U+1001: "ခ" MYANMAR LETTER KHA - // U+1002: "ဂ" MYANMAR LETTER GA - /* keylabel_to_alpha */ "\u1000\u1001\u1002", - /* morekeys_i ~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, - /* ~ morekeys_nordic_row2_11 */ - /* morekeys_punctuation */ "!autoColumnOrder!9,\u104A,.,?,!,#,),(,/,;,...,',@,:,-,\",+,\\%,&", - // U+104A: "၊" MYANMAR SIGN LITTLE SECTION - // U+104B: "။" MYANMAR SIGN SECTION - /* keyspec_tablet_comma */ "\u104A", - /* keyspec_period */ "\u104B", - /* morekeys_period */ null, - /* keyspec_tablet_period */ "\u104B", - /* keyspec_swiss_row1_11 ~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, - /* ~ keyspec_comma */ - /* morekeys_tablet_comma */ "\\,", - /* keyhintlabel_period */ "\u104A", - /* morekeys_question ~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~ keyspec_south_slavic_row3_8 */ - /* morekeys_tablet_punctuation */ "!autoColumnOrder!8,.,',#,),(,/,;,@,...,:,-,\",+,\\%,&", - }; - /* Locale nb: Norwegian Bokmål */ private static final String[] TEXTS_nb = { // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE @@ -4189,7 +4154,6 @@ public final class KeyboardTextsTable { "ml_IN" , TEXTS_ml_IN, /* 2/ 12 Malayalam (India) */ "mn_MN" , TEXTS_mn_MN, /* 2/ 12 Mongolian (Mongolia) */ "mr_IN" , TEXTS_mr_IN, /* 23/ 53 Marathi (India) */ - "my_MM" , TEXTS_my_MM, /* 8/ 98 Burmese (Myanmar) */ "nb" , TEXTS_nb, /* 11/ 55 Norwegian Bokmål */ "ne_NP" , TEXTS_ne_NP, /* 27/ 60 Nepali (Nepal) */ "nl" , TEXTS_nl, /* 9/ 13 Dutch */ diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index 46cd3b8b2..b0eae0832 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -293,8 +293,6 @@ public final class BinaryDictionary extends Dictionary { settingsValuesForSuggestion.mBlockPotentiallyOffensive); session.mNativeSuggestOptions.setSpaceAwareGestureEnabled( settingsValuesForSuggestion.mSpaceAwareGestureEnabled); - session.mNativeSuggestOptions.setAdditionalFeaturesOptions( - settingsValuesForSuggestion.mAdditionalFeaturesSettingValues); session.mNativeSuggestOptions.setWeightForLocale(weightForLocale); if (inOutWeightOfLangModelVsSpatialModel != null) { session.mInputOutputWeightOfLangModelVsSpatialModel[0] = diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java index aefefd305..e7fd99ee8 100644 --- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java +++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java @@ -18,7 +18,6 @@ package com.android.inputmethod.latin; import com.android.inputmethod.latin.common.Constants; import com.android.inputmethod.latin.common.NativeSuggestOptions; -import com.android.inputmethod.latin.settings.AdditionalFeaturesSettingUtils; import com.android.inputmethod.latin.utils.JniUtils; import java.util.Locale; @@ -44,8 +43,7 @@ public final class DicTraverseSession { public final int[] mOutputAutoCommitFirstWordConfidence = new int[1]; public final float[] mInputOutputWeightOfLangModelVsSpatialModel = new float[1]; - public final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions( - AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE); + public final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions(); private static native long setDicTraverseSessionNative(String locale, long dictSize); private static native void initDicTraverseSessionNative(long nativeDicTraverseSession, diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java index b8893a5d8..a0a1d939e 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -17,272 +17,58 @@ package com.android.inputmethod.latin; import android.content.Context; -import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; import android.view.inputmethod.InputMethodSubtype; +import android.util.Pair; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.ExpandableBinaryDictionary.UpdateEntriesForInputEventsCallback; -import com.android.inputmethod.latin.NgramContext.WordInfo; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.common.Constants; -import com.android.inputmethod.latin.personalization.ContextualDictionary; import com.android.inputmethod.latin.personalization.PersonalizationDataChunk; -import com.android.inputmethod.latin.personalization.PersonalizationDictionary; -import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; -import com.android.inputmethod.latin.utils.DistracterFilter; -import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions; -import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary; -import com.android.inputmethod.latin.utils.ExecutorUtils; import com.android.inputmethod.latin.utils.SuggestionResults; import java.io.File; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Locale; +import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** - * Facilitates interaction with different kinds of dictionaries. Provides APIs - * to instantiate and select the correct dictionaries (based on language or account), - * update entries and fetch suggestions. - * - * Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as - * a client for interacting with dictionaries. + * Interface that facilitates interaction with different kinds of dictionaries. Provides APIs to + * instantiate and select the correct dictionaries (based on language or account), update entries + * and fetch suggestions. Currently AndroidSpellCheckerService and LatinIME both use + * DictionaryFacilitator as a client for interacting with dictionaries. */ -public class DictionaryFacilitator { - // TODO: Consolidate dictionaries in native code. - public static final String TAG = DictionaryFacilitator.class.getSimpleName(); - - // 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; - // How many words we need to type in a row ({@see mConfidenceInMostProbableLanguage}) to - // declare we are confident the user is typing in the most probable language. - private static final int CONFIDENCE_THRESHOLD = 3; - - private DictionaryGroup[] mDictionaryGroups = new DictionaryGroup[] { new DictionaryGroup() }; - private DictionaryGroup mMostProbableDictionaryGroup = mDictionaryGroups[0]; - private boolean mIsUserDictEnabled = false; - private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0); - // To synchronize assigning mDictionaryGroup to ensure closing dictionaries. - private final Object mLock = new Object(); - private final DistracterFilter mDistracterFilter; - private final PersonalizationHelperForDictionaryFacilitator mPersonalizationHelper; - - private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS = - new String[] { - Dictionary.TYPE_MAIN, - Dictionary.TYPE_USER_HISTORY, - Dictionary.TYPE_PERSONALIZATION, - Dictionary.TYPE_USER, - Dictionary.TYPE_CONTACTS, - Dictionary.TYPE_CONTEXTUAL - }; - - public static final Map<String, Class<? extends ExpandableBinaryDictionary>> - DICT_TYPE_TO_CLASS = new HashMap<>(); - - 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); - DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTEXTUAL, ContextualDictionary.class); - } - - private static final String DICT_FACTORY_METHOD_NAME = "getDictionary"; - private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES = - new Class[] { Context.class, Locale.class, File.class, String.class, String.class }; - - private static final String[] SUB_DICT_TYPES = - Arrays.copyOfRange(DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS, 1 /* start */, - DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS.length); - +public interface DictionaryFacilitator { /** * Returns whether this facilitator is exactly for this list of locales. * * @param locales the list of locales to test against */ - public boolean isForLocales(final Locale[] locales) { - if (locales.length != mDictionaryGroups.length) { - return false; - } - for (final Locale locale : locales) { - boolean found = false; - for (final DictionaryGroup group : mDictionaryGroups) { - if (locale.equals(group.mLocale)) { - found = true; - break; - } - } - if (!found) { - return false; - } - } - return true; - } + boolean isForLocales(final Locale[] locales); /** * Returns whether this facilitator is exactly for this account. * * @param account the account to test against. */ - public boolean isForAccount(@Nullable final String account) { - for (final DictionaryGroup group : mDictionaryGroups) { - if (!TextUtils.equals(group.mAccount, account)) { - return false; - } - } - return true; - } + boolean isForAccount(@Nullable final String account); - /** - * A group of dictionaries that work together for a single language. - */ - private static class DictionaryGroup { - // TODO: Add null analysis annotations. - // TODO: Run evaluation to determine a reasonable value for these constants. The current - // values are ad-hoc and chosen without any particular care or methodology. - public static final float WEIGHT_FOR_MOST_PROBABLE_LANGUAGE = 1.0f; - public static final float WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.95f; - public static final float WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.6f; - - /** - * The locale associated with the dictionary group. - */ - @Nullable public final Locale mLocale; - - /** - * The user account associated with the dictionary group. - */ - @Nullable public final String mAccount; - - @Nullable private Dictionary mMainDict; - // Confidence that the most probable language is actually the language the user is - // typing in. For now, this is simply the number of times a word from this language - // has been committed in a row. - private int mConfidence = 0; - - public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; - public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; - public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap = - new ConcurrentHashMap<>(); - - public DictionaryGroup() { - this(null /* locale */, null /* mainDict */, null /* account */, - Collections.<String, ExpandableBinaryDictionary>emptyMap() /* subDicts */); - } - - public DictionaryGroup(@Nullable final Locale locale, - @Nullable final Dictionary mainDict, - @Nullable final String account, - final Map<String, ExpandableBinaryDictionary> subDicts) { - mLocale = locale; - mAccount = account; - // The main dictionary can be asynchronously loaded. - setMainDict(mainDict); - for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) { - setSubDict(entry.getKey(), entry.getValue()); - } - } - - private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) { - if (dict != null) { - mSubDictMap.put(dictType, dict); - } - } - - public void setMainDict(final Dictionary mainDict) { - // Close old dictionary if exists. Main dictionary can be assigned multiple times. - final Dictionary oldDict = mMainDict; - mMainDict = mainDict; - if (oldDict != null && mainDict != oldDict) { - oldDict.close(); - } - } - - public Dictionary getDict(final String dictType) { - if (Dictionary.TYPE_MAIN.equals(dictType)) { - return mMainDict; - } - return getSubDict(dictType); - } - - public ExpandableBinaryDictionary getSubDict(final String dictType) { - return mSubDictMap.get(dictType); - } - - public boolean hasDict(final String dictType, @Nullable final String account) { - if (Dictionary.TYPE_MAIN.equals(dictType)) { - return mMainDict != null; - } - if (Dictionary.TYPE_USER_HISTORY.equals(dictType) && - !TextUtils.equals(account, mAccount)) { - // If the dictionary type is user history, & if the account doesn't match, - // return immediately. If the account matches, continue looking it up in the - // sub dictionary map. - return false; - } - return mSubDictMap.containsKey(dictType); - } - - public void closeDict(final String dictType) { - final Dictionary dict; - if (Dictionary.TYPE_MAIN.equals(dictType)) { - dict = mMainDict; - } else { - dict = mSubDictMap.remove(dictType); - } - if (dict != null) { - dict.close(); - } - } + interface DictionaryInitializationListener { + void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); } - public interface DictionaryInitializationListener { - public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); - } - - public DictionaryFacilitator() { - mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER; - mPersonalizationHelper = null; - } - - public DictionaryFacilitator(final Context context) { - mDistracterFilter = new DistracterFilterCheckingExactMatchesAndSuggestions(context); - mPersonalizationHelper = - new PersonalizationHelperForDictionaryFacilitator(context, mDistracterFilter); - } - - public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { - mDistracterFilter.updateEnabledSubtypes(enabledSubtypes); - mPersonalizationHelper.updateEnabledSubtypes(enabledSubtypes); - } + void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes); // TODO: remove this, it's confusing with seamless multiple language switching - public void setIsMonolingualUser(final boolean isMonolingualUser) { - mPersonalizationHelper.setIsMonolingualUser(isMonolingualUser); - } + void setIsMonolingualUser(final boolean isMonolingualUser); - public boolean isActive() { - return null != mDictionaryGroups[0].mLocale; - } + boolean isActive(); /** * Returns the most probable locale among all currently active locales. BE CAREFUL using this. @@ -292,660 +78,96 @@ public class DictionaryFacilitator { * string. * @return the most probable locale */ - public Locale getMostProbableLocale() { - return getDictionaryGroupForMostProbableLanguage().mLocale; - } + Locale getMostProbableLocale(); - public Locale[] getLocales() { - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - final Locale[] locales = new Locale[dictionaryGroups.length]; - for (int i = 0; i < dictionaryGroups.length; ++i) { - locales[i] = dictionaryGroups[i].mLocale; - } - return locales; - } + Locale[] getLocales(); - private DictionaryGroup getDictionaryGroupForMostProbableLanguage() { - return mMostProbableDictionaryGroup; - } + void switchMostProbableLanguage(@Nullable final Locale locale); - public void switchMostProbableLanguage(@Nullable final Locale locale) { - if (null == locale) { - // In many cases, there is no locale to a committed word. For example, a typed word - // that is in none of the currently active dictionaries but still does not - // auto-correct to anything has no locale. In this case we simply do not change - // the most probable language and do not touch confidence. - return; - } - final DictionaryGroup newMostProbableDictionaryGroup = - findDictionaryGroupWithLocale(mDictionaryGroups, locale); - if (null == newMostProbableDictionaryGroup) { - // It seems this may happen as a race condition; pressing the globe key and space - // in quick succession could commit a word out of a dictionary that's not in the - // facilitator any more. In this case, just not changing things is fine. - return; - } - if (newMostProbableDictionaryGroup == mMostProbableDictionaryGroup) { - ++newMostProbableDictionaryGroup.mConfidence; - } else { - mMostProbableDictionaryGroup.mWeightForTypingInLocale = - DictionaryGroup.WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE; - mMostProbableDictionaryGroup.mWeightForGesturingInLocale = - DictionaryGroup.WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE; - mMostProbableDictionaryGroup.mConfidence = 0; - newMostProbableDictionaryGroup.mWeightForTypingInLocale = - DictionaryGroup.WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; - newMostProbableDictionaryGroup.mWeightForGesturingInLocale = - DictionaryGroup.WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; - mMostProbableDictionaryGroup = newMostProbableDictionaryGroup; - } - } + boolean isConfidentAboutCurrentLanguageBeing(final Locale mLocale); - public boolean isConfidentAboutCurrentLanguageBeing(final Locale mLocale) { - final DictionaryGroup mostProbableDictionaryGroup = mMostProbableDictionaryGroup; - if (!mostProbableDictionaryGroup.mLocale.equals(mLocale)) { - return false; - } - if (mDictionaryGroups.length <= 1) { - return true; - } - return mostProbableDictionaryGroup.mConfidence >= CONFIDENCE_THRESHOLD; - } - - @Nullable - private static ExpandableBinaryDictionary getSubDict(final String dictType, - final Context context, final Locale locale, final File dictFile, - final String dictNamePrefix, @Nullable final String account) { - 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 */, - new Object[] { context, locale, dictFile, dictNamePrefix, account }); - return (ExpandableBinaryDictionary) dict; - } catch (final NoSuchMethodException | SecurityException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException e) { - Log.e(TAG, "Cannot create dictionary: " + dictType, e); - return null; - } - } - - public void resetDictionaries(final Context context, final Locale[] newLocales, + void resetDictionaries(final Context context, final Locale[] newLocales, final boolean useContactsDict, final boolean usePersonalizedDicts, final boolean forceReloadMainDictionary, @Nullable final String account, - final DictionaryInitializationListener listener) { - resetDictionariesWithDictNamePrefix(context, newLocales, useContactsDict, - usePersonalizedDicts, forceReloadMainDictionary, listener, "" /* dictNamePrefix */, - account); - } + final DictionaryInitializationListener listener); - @Nullable - static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup[] dictionaryGroups, - final Locale locale) { - for (DictionaryGroup dictionaryGroup : dictionaryGroups) { - if (locale.equals(dictionaryGroup.mLocale)) { - return dictionaryGroup; - } - } - return null; - } - - public void resetDictionariesWithDictNamePrefix(final Context context, + void resetDictionariesWithDictNamePrefix(final Context context, final Locale[] newLocales, final boolean useContactsDict, final boolean usePersonalizedDicts, final boolean forceReloadMainDictionary, @Nullable final DictionaryInitializationListener listener, final String dictNamePrefix, - @Nullable final String account) { - final HashMap<Locale, ArrayList<String>> existingDictionariesToCleanup = new HashMap<>(); - // TODO: Make subDictTypesToUse configurable by resource or a static final list. - final HashSet<String> subDictTypesToUse = new HashSet<>(); - subDictTypesToUse.add(Dictionary.TYPE_USER); - if (useContactsDict) { - subDictTypesToUse.add(Dictionary.TYPE_CONTACTS); - } - if (usePersonalizedDicts) { - subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); - subDictTypesToUse.add(Dictionary.TYPE_PERSONALIZATION); - subDictTypesToUse.add(Dictionary.TYPE_CONTEXTUAL); - } - - // Gather all dictionaries. We'll remove them from the list to clean up later. - for (final Locale newLocale : newLocales) { - final ArrayList<String> dictTypeForLocale = new ArrayList<>(); - existingDictionariesToCleanup.put(newLocale, dictTypeForLocale); - final DictionaryGroup currentDictionaryGroupForLocale = - findDictionaryGroupWithLocale(mDictionaryGroups, newLocale); - if (null == currentDictionaryGroupForLocale) { - continue; - } - for (final String dictType : SUB_DICT_TYPES) { - if (currentDictionaryGroupForLocale.hasDict(dictType, account)) { - dictTypeForLocale.add(dictType); - } - } - if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) { - dictTypeForLocale.add(Dictionary.TYPE_MAIN); - } - } - - final DictionaryGroup[] newDictionaryGroups = new DictionaryGroup[newLocales.length]; - for (int i = 0; i < newLocales.length; ++i) { - final Locale newLocale = newLocales[i]; - final DictionaryGroup dictionaryGroupForLocale = - findDictionaryGroupWithLocale(mDictionaryGroups, newLocale); - final ArrayList<String> dictTypesToCleanupForLocale = - existingDictionariesToCleanup.get(newLocale); - final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale); - - final Dictionary mainDict; - if (forceReloadMainDictionary || noExistingDictsForThisLocale - || !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) { - mainDict = null; - } else { - mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN); - dictTypesToCleanupForLocale.remove(Dictionary.TYPE_MAIN); - } - - final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); - for (final String subDictType : subDictTypesToUse) { - final ExpandableBinaryDictionary subDict; - if (noExistingDictsForThisLocale - || !dictionaryGroupForLocale.hasDict(subDictType, account)) { - // Create a new dictionary. - subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */, - dictNamePrefix, account); - } else { - // Reuse the existing dictionary, and don't close it at the end - subDict = dictionaryGroupForLocale.getSubDict(subDictType); - dictTypesToCleanupForLocale.remove(subDictType); - } - subDicts.put(subDictType, subDict); - } - newDictionaryGroups[i] = new DictionaryGroup(newLocale, mainDict, account, subDicts); - } - - // Replace Dictionaries. - final DictionaryGroup[] oldDictionaryGroups; - synchronized (mLock) { - oldDictionaryGroups = mDictionaryGroups; - mDictionaryGroups = newDictionaryGroups; - mMostProbableDictionaryGroup = newDictionaryGroups[0]; - mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context); - if (hasAtLeastOneUninitializedMainDictionary()) { - asyncReloadUninitializedMainDictionaries(context, newLocales, listener); - } - } - if (listener != null) { - listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary()); - } - - // Clean up old dictionaries. - for (final Locale localeToCleanUp : existingDictionariesToCleanup.keySet()) { - final ArrayList<String> dictTypesToCleanUp = - existingDictionariesToCleanup.get(localeToCleanUp); - final DictionaryGroup dictionarySetToCleanup = - findDictionaryGroupWithLocale(oldDictionaryGroups, localeToCleanUp); - for (final String dictType : dictTypesToCleanUp) { - dictionarySetToCleanup.closeDict(dictType); - } - } - } - - private void asyncReloadUninitializedMainDictionaries(final Context context, - final Locale[] locales, final DictionaryInitializationListener listener) { - final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); - mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary; - ExecutorUtils.getExecutor("InitializeBinaryDictionary").execute(new Runnable() { - @Override - public void run() { - doReloadUninitializedMainDictionaries( - context, locales, listener, latchForWaitingLoadingMainDictionary); - } - }); - } - - void doReloadUninitializedMainDictionaries(final Context context, final Locale[] locales, - final DictionaryInitializationListener listener, - final CountDownLatch latchForWaitingLoadingMainDictionary) { - for (final Locale locale : locales) { - final DictionaryGroup dictionaryGroup = - findDictionaryGroupWithLocale(mDictionaryGroups, locale); - if (null == dictionaryGroup) { - // This should never happen, but better safe than crashy - Log.w(TAG, "Expected a dictionary group for " + locale + " but none found"); - continue; - } - final Dictionary mainDict = - DictionaryFactory.createMainDictionaryFromManager(context, locale); - synchronized (mLock) { - if (locale.equals(dictionaryGroup.mLocale)) { - dictionaryGroup.setMainDict(mainDict); - } else { - // Dictionary facilitator has been reset for another locale. - mainDict.close(); - } - } - } - if (listener != null) { - listener.onUpdateMainDictionaryAvailability( - hasAtLeastOneInitializedMainDictionary()); - } - latchForWaitingLoadingMainDictionary.countDown(); - } + @Nullable final String account); @UsedForTesting - public void resetDictionariesForTesting(final Context context, final Locale[] locales, + void resetDictionariesForTesting(final Context context, final Locale[] locales, final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles, final Map<String, Map<String, String>> additionalDictAttributes, - @Nullable final String account) { - Dictionary mainDictionary = null; - final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); - - final DictionaryGroup[] dictionaryGroups = new DictionaryGroup[locales.length]; - for (int i = 0; i < locales.length; ++i) { - final Locale locale = locales[i]; - for (final String dictType : dictionaryTypes) { - if (dictType.equals(Dictionary.TYPE_MAIN)) { - mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context, - locale); - } else { - final File dictFile = dictionaryFiles.get(dictType); - final ExpandableBinaryDictionary dict = getSubDict( - dictType, context, locale, dictFile, "" /* dictNamePrefix */, account); - if (additionalDictAttributes.containsKey(dictType)) { - dict.clearAndFlushDictionaryWithAdditionalAttributes( - additionalDictAttributes.get(dictType)); - } - if (dict == null) { - throw new RuntimeException("Unknown dictionary type: " + dictType); - } - dict.reloadDictionaryIfRequired(); - dict.waitAllTasksForTests(); - subDicts.put(dictType, dict); - } - } - dictionaryGroups[i] = new DictionaryGroup(locale, mainDictionary, account, subDicts); - } - mDictionaryGroups = dictionaryGroups; - mMostProbableDictionaryGroup = dictionaryGroups[0]; - } + @Nullable final String account); - public void closeDictionaries() { - final DictionaryGroup[] dictionaryGroups; - synchronized (mLock) { - dictionaryGroups = mDictionaryGroups; - mMostProbableDictionaryGroup = new DictionaryGroup(); - mDictionaryGroups = new DictionaryGroup[] { mMostProbableDictionaryGroup }; - } - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - dictionaryGroup.closeDict(dictType); - } - } - mDistracterFilter.close(); - if (mPersonalizationHelper != null) { - mPersonalizationHelper.close(); - } - } + void closeDictionaries(); @UsedForTesting - public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { - return mMostProbableDictionaryGroup.getSubDict(dictName); - } + ExpandableBinaryDictionary getSubDictForTesting(final String dictName); - // The main dictionaries are loaded asynchronously. Don't cache the return value + // The main dictionaries are loaded asynchronously. Don't cache the return value // of these methods. - public boolean hasAtLeastOneInitializedMainDictionary() { - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN); - if (mainDict != null && mainDict.isInitialized()) { - return true; - } - } - return false; - } + boolean hasAtLeastOneInitializedMainDictionary(); - public boolean hasAtLeastOneUninitializedMainDictionary() { - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN); - if (mainDict == null || !mainDict.isInitialized()) { - return true; - } - } - return false; - } + boolean hasAtLeastOneUninitializedMainDictionary(); - public boolean hasPersonalizationDictionary() { - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - if (dictionaryGroup.hasDict(Dictionary.TYPE_PERSONALIZATION, null /* account */)) { - return true; - } - } - return false; - } + boolean hasPersonalizationDictionary(); - public void flushPersonalizationDictionary() { - final HashSet<ExpandableBinaryDictionary> personalizationDictsUsedForSuggestion = - new HashSet<>(); - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - final ExpandableBinaryDictionary personalizationDictUsedForSuggestion = - dictionaryGroup.getSubDict(Dictionary.TYPE_PERSONALIZATION); - personalizationDictsUsedForSuggestion.add(personalizationDictUsedForSuggestion); - } - mPersonalizationHelper.flushPersonalizationDictionariesToUpdate( - personalizationDictsUsedForSuggestion); - mDistracterFilter.close(); - } + void flushPersonalizationDictionary(); - public void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit) - throws InterruptedException { - mLatchForWaitingLoadingMainDictionaries.await(timeout, unit); - } + void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit) + throws InterruptedException; @UsedForTesting - public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) - throws InterruptedException { - waitForLoadingMainDictionaries(timeout, unit); - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - for (final ExpandableBinaryDictionary dict : dictionaryGroup.mSubDictMap.values()) { - dict.waitAllTasksForTests(); - } - } - } + void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) + throws InterruptedException; - public boolean isUserDictionaryEnabled() { - return mIsUserDictEnabled; - } + boolean isUserDictionaryEnabled(); - public void addWordToUserDictionary(final Context context, final String word) { - final Locale locale = getMostProbableLocale(); - if (locale == null) { - return; - } - // TODO: add a toast telling what language this is being added to? - UserBinaryDictionary.addWordToUserDictionary(context, locale, word); - } + void addWordToUserDictionary(final Context context, final String word); - public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, + void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, @Nonnull final NgramContext ngramContext, final int timeStampInSeconds, - final boolean blockPotentiallyOffensive) { - final DictionaryGroup dictionaryGroup = getDictionaryGroupForMostProbableLanguage(); - final String[] words = suggestion.split(Constants.WORD_SEPARATOR); - NgramContext ngramContextForCurrentWord = ngramContext; - for (int i = 0; i < words.length; i++) { - final String currentWord = words[i]; - final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false; - addWordToUserHistory(dictionaryGroup, ngramContextForCurrentWord, currentWord, - wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive); - ngramContextForCurrentWord = - ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord)); - } - } - - private void addWordToUserHistory(final DictionaryGroup dictionaryGroup, - final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized, - final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { - final ExpandableBinaryDictionary userHistoryDictionary = - dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY); - if (userHistoryDictionary == null - || !isConfidentAboutCurrentLanguageBeing(userHistoryDictionary.mLocale)) { - return; - } - final int maxFreq = getFrequency(word); - if (maxFreq == 0 && blockPotentiallyOffensive) { - return; - } - final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); - final String secondWord; - if (wasAutoCapitalized) { - if (isValidWord(word, false /* ignoreCase */) - && !isValidWord(lowerCasedWord, false /* ignoreCase */)) { - // 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. - secondWord = word; - } 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. - secondWord = lowerCasedWord; - } - } 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. - final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN, - null /* account */) ? - dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) : - Dictionary.NOT_A_PROBABILITY; - if (maxFreq < lowerCaseFreqInMainDict - && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) { - // Use lower cased word as the word can be a distracter of the popular word. - secondWord = lowerCasedWord; - } else { - secondWord = word; - } - } - // 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 boolean isValid = maxFreq > 0; - UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, secondWord, - isValid, timeStampInSeconds, - new DistracterFilterCheckingIsInDictionary( - mDistracterFilter, userHistoryDictionary)); - } + final boolean blockPotentiallyOffensive); - private void removeWord(final String dictName, final String word) { - final ExpandableBinaryDictionary dictionary = - getDictionaryGroupForMostProbableLanguage().getSubDict(dictName); - if (dictionary != null) { - dictionary.removeUnigramEntryDynamically(word); - } - } - - public void removeWordFromPersonalizedDicts(final String word) { - removeWord(Dictionary.TYPE_USER_HISTORY, word); - removeWord(Dictionary.TYPE_PERSONALIZATION, word); - removeWord(Dictionary.TYPE_CONTEXTUAL, word); - } + void removeWordFromPersonalizedDicts(final String word); // TODO: Revise the way to fusion suggestion results. - public SuggestionResults getSuggestionResults(final WordComposer composer, + SuggestionResults getSuggestionResults(final WordComposer composer, final NgramContext ngramContext, final long proximityInfoHandle, - final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) { - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - final SuggestionResults suggestionResults = new SuggestionResults( - SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext()); - final float[] weightOfLangModelVsSpatialModel = - new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL }; - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - final Dictionary dictionary = dictionaryGroup.getDict(dictType); - if (null == dictionary) continue; - final float weightForLocale = composer.isBatchMode() - ? dictionaryGroup.mWeightForGesturingInLocale - : dictionaryGroup.mWeightForTypingInLocale; - final ArrayList<SuggestedWordInfo> dictionarySuggestions = - dictionary.getSuggestions(composer.getComposedDataSnapshot(), ngramContext, - proximityInfoHandle, settingsValuesForSuggestion, sessionId, - weightForLocale, weightOfLangModelVsSpatialModel); - if (null == dictionarySuggestions) continue; - suggestionResults.addAll(dictionarySuggestions); - if (null != suggestionResults.mRawSuggestions) { - suggestionResults.mRawSuggestions.addAll(dictionarySuggestions); - } - } - } - return suggestionResults; - } + final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId); - public boolean isValidWord(final String word, final boolean ignoreCase) { - if (TextUtils.isEmpty(word)) { - return false; - } - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - if (dictionaryGroup.mLocale == null) { - continue; - } - final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); - for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - final Dictionary dictionary = dictionaryGroup.getDict(dictType); - // 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; - } + boolean isValidWord(final String word, final boolean ignoreCase); - private int getFrequencyInternal(final String word, - final boolean isGettingMaxFrequencyOfExactMatches) { - if (TextUtils.isEmpty(word)) { - return Dictionary.NOT_A_PROBABILITY; - } - int maxFreq = Dictionary.NOT_A_PROBABILITY; - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - final Dictionary dictionary = dictionaryGroup.getDict(dictType); - if (dictionary == null) continue; - final int tempFreq; - if (isGettingMaxFrequencyOfExactMatches) { - tempFreq = dictionary.getMaxFrequencyOfExactMatches(word); - } else { - tempFreq = dictionary.getFrequency(word); - } - if (tempFreq >= maxFreq) { - maxFreq = tempFreq; - } - } - } - return maxFreq; - } + int getFrequency(final String word); - public int getFrequency(final String word) { - return getFrequencyInternal(word, false /* isGettingMaxFrequencyOfExactMatches */); - } - - public int getMaxFrequencyOfExactMatches(final String word) { - return getFrequencyInternal(word, true /* isGettingMaxFrequencyOfExactMatches */); - } + int getMaxFrequencyOfExactMatches(final String word); - private void clearSubDictionary(final String dictName) { - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictName); - if (dictionary != null) { - dictionary.clear(); - } - } - } - - public void clearUserHistoryDictionary() { - clearSubDictionary(Dictionary.TYPE_USER_HISTORY); - } + void clearUserHistoryDictionary(); // This method gets called only when the IME receives a notification to remove the // personalization dictionary. - public void clearPersonalizationDictionary() { - clearSubDictionary(Dictionary.TYPE_PERSONALIZATION); - mPersonalizationHelper.clearDictionariesToUpdate(); - } + void clearPersonalizationDictionary(); - public void clearContextualDictionary() { - clearSubDictionary(Dictionary.TYPE_CONTEXTUAL); - } + void clearContextualDictionary(); - public void addEntriesToPersonalizationDictionary( + void addEntriesToPersonalizationDictionary( final PersonalizationDataChunk personalizationDataChunk, final SpacingAndPunctuations spacingAndPunctuations, - final UpdateEntriesForInputEventsCallback callback) { - mPersonalizationHelper.updateEntriesOfPersonalizationDictionaries( - getMostProbableLocale(), personalizationDataChunk, spacingAndPunctuations, - callback); - } + final UpdateEntriesForInputEventsCallback callback); @UsedForTesting - public void addPhraseToContextualDictionary(final String[] phrase, final int probability, - final int bigramProbabilityForWords, final int bigramProbabilityForPhrases) { - // TODO: we're inserting the phrase into the dictionary for the active language. Rethink - // this a bit from a theoretical point of view. - final ExpandableBinaryDictionary contextualDict = - getDictionaryGroupForMostProbableLanguage().getSubDict(Dictionary.TYPE_CONTEXTUAL); - if (contextualDict == null) { - return; - } - NgramContext ngramContext = NgramContext.BEGINNING_OF_SENTENCE; - for (int i = 0; i < phrase.length; i++) { - final String[] subPhrase = Arrays.copyOfRange(phrase, i /* start */, phrase.length); - final String subPhraseStr = TextUtils.join(Constants.WORD_SEPARATOR, subPhrase); - contextualDict.addUnigramEntryWithCheckingDistracter( - subPhraseStr, probability, null /* shortcutTarget */, - Dictionary.NOT_A_PROBABILITY /* shortcutFreq */, - false /* isNotAWord */, false /* isPossiblyOffensive */, - BinaryDictionary.NOT_A_VALID_TIMESTAMP, - DistracterFilter.EMPTY_DISTRACTER_FILTER); - contextualDict.addNgramEntry(ngramContext, subPhraseStr, - bigramProbabilityForPhrases, BinaryDictionary.NOT_A_VALID_TIMESTAMP); - - if (i < phrase.length - 1) { - contextualDict.addUnigramEntryWithCheckingDistracter( - phrase[i], probability, null /* shortcutTarget */, - Dictionary.NOT_A_PROBABILITY /* shortcutFreq */, - false /* isNotAWord */, false /* isPossiblyOffensive */, - BinaryDictionary.NOT_A_VALID_TIMESTAMP, - DistracterFilter.EMPTY_DISTRACTER_FILTER); - contextualDict.addNgramEntry(ngramContext, phrase[i], - bigramProbabilityForWords, BinaryDictionary.NOT_A_VALID_TIMESTAMP); - } - ngramContext = - ngramContext.getNextNgramContext(new NgramContext.WordInfo(phrase[i])); - } - } + void addPhraseToContextualDictionary(final String[] phrase, final int probability, + final int bigramProbabilityForWords, final int bigramProbabilityForPhrases); - public void dumpDictionaryForDebug(final String dictName) { - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - final ExpandableBinaryDictionary dictToDump = dictionaryGroup.getSubDict(dictName); - if (dictToDump == null) { - Log.e(TAG, "Cannot dump " + dictName + ". " - + "The dictionary is not being used for suggestion or cannot be dumped."); - return; - } - dictToDump.dumpAllWordsForDebug(); - } - } + void dumpDictionaryForDebug(final String dictName); - public ArrayList<Pair<String, DictionaryStats>> getStatsOfEnabledSubDicts() { - final ArrayList<Pair<String, DictionaryStats>> statsOfEnabledSubDicts = new ArrayList<>(); - final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; - for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { - for (final String dictType : SUB_DICT_TYPES) { - final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictType); - if (dictionary == null) continue; - statsOfEnabledSubDicts.add(new Pair<>(dictType, dictionary.getDictionaryStats())); - } - } - return statsOfEnabledSubDicts; - } + ArrayList<Pair<String, DictionaryStats>> getStatsOfEnabledSubDicts(); } diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java new file mode 100644 index 000000000..167501118 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java @@ -0,0 +1,947 @@ +/* + * 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 android.util.Pair; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.ExpandableBinaryDictionary.UpdateEntriesForInputEventsCallback; +import com.android.inputmethod.latin.NgramContext.WordInfo; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.common.Constants; +import com.android.inputmethod.latin.personalization.ContextualDictionary; +import com.android.inputmethod.latin.personalization.PersonalizationDataChunk; +import com.android.inputmethod.latin.personalization.PersonalizationDictionary; +import com.android.inputmethod.latin.personalization.UserHistoryDictionary; +import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; +import com.android.inputmethod.latin.utils.DistracterFilter; +import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions; +import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary; +import com.android.inputmethod.latin.utils.ExecutorUtils; +import com.android.inputmethod.latin.utils.SuggestionResults; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Facilitates interaction with different kinds of dictionaries. Provides APIs + * to instantiate and select the correct dictionaries (based on language or account), + * update entries and fetch suggestions. + * + * Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as + * a client for interacting with dictionaries. + */ +public class DictionaryFacilitatorImpl implements DictionaryFacilitator { + // TODO: Consolidate dictionaries in native code. + public static final String TAG = DictionaryFacilitatorImpl.class.getSimpleName(); + + // 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; + // How many words we need to type in a row ({@see mConfidenceInMostProbableLanguage}) to + // declare we are confident the user is typing in the most probable language. + private static final int CONFIDENCE_THRESHOLD = 3; + + private DictionaryGroup[] mDictionaryGroups = new DictionaryGroup[] { new DictionaryGroup() }; + private DictionaryGroup mMostProbableDictionaryGroup = mDictionaryGroups[0]; + private boolean mIsUserDictEnabled = false; + private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0); + // To synchronize assigning mDictionaryGroup to ensure closing dictionaries. + private final Object mLock = new Object(); + private final DistracterFilter mDistracterFilter; + private final PersonalizationHelperForDictionaryFacilitator mPersonalizationHelper; + + private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS = + new String[] { + Dictionary.TYPE_MAIN, + Dictionary.TYPE_USER_HISTORY, + Dictionary.TYPE_PERSONALIZATION, + Dictionary.TYPE_USER, + Dictionary.TYPE_CONTACTS, + Dictionary.TYPE_CONTEXTUAL + }; + + public static final Map<String, Class<? extends ExpandableBinaryDictionary>> + DICT_TYPE_TO_CLASS = new HashMap<>(); + + 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); + DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTEXTUAL, ContextualDictionary.class); + } + + private static final String DICT_FACTORY_METHOD_NAME = "getDictionary"; + private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES = + new Class[] { Context.class, Locale.class, File.class, String.class, String.class }; + + private static final String[] SUB_DICT_TYPES = + Arrays.copyOfRange(DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS, 1 /* start */, + DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS.length); + + /** + * Returns whether this facilitator is exactly for this list of locales. + * + * @param locales the list of locales to test against + */ + public boolean isForLocales(final Locale[] locales) { + if (locales.length != mDictionaryGroups.length) { + return false; + } + for (final Locale locale : locales) { + boolean found = false; + for (final DictionaryGroup group : mDictionaryGroups) { + if (locale.equals(group.mLocale)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + /** + * Returns whether this facilitator is exactly for this account. + * + * @param account the account to test against. + */ + public boolean isForAccount(@Nullable final String account) { + for (final DictionaryGroup group : mDictionaryGroups) { + if (!TextUtils.equals(group.mAccount, account)) { + return false; + } + } + return true; + } + + /** + * A group of dictionaries that work together for a single language. + */ + private static class DictionaryGroup { + // TODO: Add null analysis annotations. + // TODO: Run evaluation to determine a reasonable value for these constants. The current + // values are ad-hoc and chosen without any particular care or methodology. + public static final float WEIGHT_FOR_MOST_PROBABLE_LANGUAGE = 1.0f; + public static final float WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.95f; + public static final float WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.6f; + + /** + * The locale associated with the dictionary group. + */ + @Nullable public final Locale mLocale; + + /** + * The user account associated with the dictionary group. + */ + @Nullable public final String mAccount; + + @Nullable private Dictionary mMainDict; + // Confidence that the most probable language is actually the language the user is + // typing in. For now, this is simply the number of times a word from this language + // has been committed in a row. + private int mConfidence = 0; + + public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; + public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; + public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap = + new ConcurrentHashMap<>(); + + public DictionaryGroup() { + this(null /* locale */, null /* mainDict */, null /* account */, + Collections.<String, ExpandableBinaryDictionary>emptyMap() /* subDicts */); + } + + public DictionaryGroup(@Nullable final Locale locale, + @Nullable final Dictionary mainDict, + @Nullable final String account, + final Map<String, ExpandableBinaryDictionary> subDicts) { + mLocale = locale; + mAccount = account; + // The main dictionary can be asynchronously loaded. + setMainDict(mainDict); + for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) { + setSubDict(entry.getKey(), entry.getValue()); + } + } + + private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) { + if (dict != null) { + mSubDictMap.put(dictType, dict); + } + } + + public void setMainDict(final Dictionary mainDict) { + // Close old dictionary if exists. Main dictionary can be assigned multiple times. + final Dictionary oldDict = mMainDict; + mMainDict = mainDict; + if (oldDict != null && mainDict != oldDict) { + oldDict.close(); + } + } + + public Dictionary getDict(final String dictType) { + if (Dictionary.TYPE_MAIN.equals(dictType)) { + return mMainDict; + } + return getSubDict(dictType); + } + + public ExpandableBinaryDictionary getSubDict(final String dictType) { + return mSubDictMap.get(dictType); + } + + public boolean hasDict(final String dictType, @Nullable final String account) { + if (Dictionary.TYPE_MAIN.equals(dictType)) { + return mMainDict != null; + } + if (Dictionary.TYPE_USER_HISTORY.equals(dictType) && + !TextUtils.equals(account, mAccount)) { + // If the dictionary type is user history, & if the account doesn't match, + // return immediately. If the account matches, continue looking it up in the + // sub dictionary map. + return false; + } + return mSubDictMap.containsKey(dictType); + } + + public void closeDict(final String dictType) { + final Dictionary dict; + if (Dictionary.TYPE_MAIN.equals(dictType)) { + dict = mMainDict; + } else { + dict = mSubDictMap.remove(dictType); + } + if (dict != null) { + dict.close(); + } + } + } + + public DictionaryFacilitatorImpl() { + mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER; + mPersonalizationHelper = null; + } + + public DictionaryFacilitatorImpl(final Context context) { + mDistracterFilter = new DistracterFilterCheckingExactMatchesAndSuggestions(context); + mPersonalizationHelper = + new PersonalizationHelperForDictionaryFacilitator(context, mDistracterFilter); + } + + public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { + mDistracterFilter.updateEnabledSubtypes(enabledSubtypes); + mPersonalizationHelper.updateEnabledSubtypes(enabledSubtypes); + } + + // TODO: remove this, it's confusing with seamless multiple language switching + public void setIsMonolingualUser(final boolean isMonolingualUser) { + mPersonalizationHelper.setIsMonolingualUser(isMonolingualUser); + } + + public boolean isActive() { + return null != mDictionaryGroups[0].mLocale; + } + + /** + * Returns the most probable locale among all currently active locales. BE CAREFUL using this. + * + * DO NOT USE THIS just because it's convenient. Use it when it's correct, for example when + * choosing what dictionary to put a word in, or when changing the capitalization of a typed + * string. + * @return the most probable locale + */ + public Locale getMostProbableLocale() { + return getDictionaryGroupForMostProbableLanguage().mLocale; + } + + public Locale[] getLocales() { + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + final Locale[] locales = new Locale[dictionaryGroups.length]; + for (int i = 0; i < dictionaryGroups.length; ++i) { + locales[i] = dictionaryGroups[i].mLocale; + } + return locales; + } + + private DictionaryGroup getDictionaryGroupForMostProbableLanguage() { + return mMostProbableDictionaryGroup; + } + + public void switchMostProbableLanguage(@Nullable final Locale locale) { + if (null == locale) { + // In many cases, there is no locale to a committed word. For example, a typed word + // that is in none of the currently active dictionaries but still does not + // auto-correct to anything has no locale. In this case we simply do not change + // the most probable language and do not touch confidence. + return; + } + final DictionaryGroup newMostProbableDictionaryGroup = + findDictionaryGroupWithLocale(mDictionaryGroups, locale); + if (null == newMostProbableDictionaryGroup) { + // It seems this may happen as a race condition; pressing the globe key and space + // in quick succession could commit a word out of a dictionary that's not in the + // facilitator any more. In this case, just not changing things is fine. + return; + } + if (newMostProbableDictionaryGroup == mMostProbableDictionaryGroup) { + ++newMostProbableDictionaryGroup.mConfidence; + } else { + mMostProbableDictionaryGroup.mWeightForTypingInLocale = + DictionaryGroup.WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE; + mMostProbableDictionaryGroup.mWeightForGesturingInLocale = + DictionaryGroup.WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE; + mMostProbableDictionaryGroup.mConfidence = 0; + newMostProbableDictionaryGroup.mWeightForTypingInLocale = + DictionaryGroup.WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; + newMostProbableDictionaryGroup.mWeightForGesturingInLocale = + DictionaryGroup.WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; + mMostProbableDictionaryGroup = newMostProbableDictionaryGroup; + } + } + + public boolean isConfidentAboutCurrentLanguageBeing(final Locale mLocale) { + final DictionaryGroup mostProbableDictionaryGroup = mMostProbableDictionaryGroup; + if (!mostProbableDictionaryGroup.mLocale.equals(mLocale)) { + return false; + } + if (mDictionaryGroups.length <= 1) { + return true; + } + return mostProbableDictionaryGroup.mConfidence >= CONFIDENCE_THRESHOLD; + } + + @Nullable + private static ExpandableBinaryDictionary getSubDict(final String dictType, + final Context context, final Locale locale, final File dictFile, + final String dictNamePrefix, @Nullable final String account) { + 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 */, + new Object[] { context, locale, dictFile, dictNamePrefix, account }); + return (ExpandableBinaryDictionary) dict; + } catch (final NoSuchMethodException | SecurityException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException e) { + Log.e(TAG, "Cannot create dictionary: " + dictType, e); + return null; + } + } + + public void resetDictionaries(final Context context, final Locale[] newLocales, + final boolean useContactsDict, final boolean usePersonalizedDicts, + final boolean forceReloadMainDictionary, + @Nullable final String account, + final DictionaryInitializationListener listener) { + resetDictionariesWithDictNamePrefix(context, newLocales, useContactsDict, + usePersonalizedDicts, forceReloadMainDictionary, listener, "" /* dictNamePrefix */, + account); + } + + @Nullable + static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup[] dictionaryGroups, + final Locale locale) { + for (DictionaryGroup dictionaryGroup : dictionaryGroups) { + if (locale.equals(dictionaryGroup.mLocale)) { + return dictionaryGroup; + } + } + return null; + } + + public void resetDictionariesWithDictNamePrefix(final Context context, + final Locale[] newLocales, + final boolean useContactsDict, + final boolean usePersonalizedDicts, + final boolean forceReloadMainDictionary, + @Nullable final DictionaryInitializationListener listener, + final String dictNamePrefix, + @Nullable final String account) { + final HashMap<Locale, ArrayList<String>> existingDictionariesToCleanup = new HashMap<>(); + // TODO: Make subDictTypesToUse configurable by resource or a static final list. + final HashSet<String> subDictTypesToUse = new HashSet<>(); + subDictTypesToUse.add(Dictionary.TYPE_USER); + if (useContactsDict) { + subDictTypesToUse.add(Dictionary.TYPE_CONTACTS); + } + if (usePersonalizedDicts) { + subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); + subDictTypesToUse.add(Dictionary.TYPE_PERSONALIZATION); + subDictTypesToUse.add(Dictionary.TYPE_CONTEXTUAL); + } + + // Gather all dictionaries. We'll remove them from the list to clean up later. + for (final Locale newLocale : newLocales) { + final ArrayList<String> dictTypeForLocale = new ArrayList<>(); + existingDictionariesToCleanup.put(newLocale, dictTypeForLocale); + final DictionaryGroup currentDictionaryGroupForLocale = + findDictionaryGroupWithLocale(mDictionaryGroups, newLocale); + if (null == currentDictionaryGroupForLocale) { + continue; + } + for (final String dictType : SUB_DICT_TYPES) { + if (currentDictionaryGroupForLocale.hasDict(dictType, account)) { + dictTypeForLocale.add(dictType); + } + } + if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) { + dictTypeForLocale.add(Dictionary.TYPE_MAIN); + } + } + + final DictionaryGroup[] newDictionaryGroups = new DictionaryGroup[newLocales.length]; + for (int i = 0; i < newLocales.length; ++i) { + final Locale newLocale = newLocales[i]; + final DictionaryGroup dictionaryGroupForLocale = + findDictionaryGroupWithLocale(mDictionaryGroups, newLocale); + final ArrayList<String> dictTypesToCleanupForLocale = + existingDictionariesToCleanup.get(newLocale); + final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale); + + final Dictionary mainDict; + if (forceReloadMainDictionary || noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) { + mainDict = null; + } else { + mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN); + dictTypesToCleanupForLocale.remove(Dictionary.TYPE_MAIN); + } + + final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); + for (final String subDictType : subDictTypesToUse) { + final ExpandableBinaryDictionary subDict; + if (noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(subDictType, account)) { + // Create a new dictionary. + subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */, + dictNamePrefix, account); + } else { + // Reuse the existing dictionary, and don't close it at the end + subDict = dictionaryGroupForLocale.getSubDict(subDictType); + dictTypesToCleanupForLocale.remove(subDictType); + } + subDicts.put(subDictType, subDict); + } + newDictionaryGroups[i] = new DictionaryGroup(newLocale, mainDict, account, subDicts); + } + + // Replace Dictionaries. + final DictionaryGroup[] oldDictionaryGroups; + synchronized (mLock) { + oldDictionaryGroups = mDictionaryGroups; + mDictionaryGroups = newDictionaryGroups; + mMostProbableDictionaryGroup = newDictionaryGroups[0]; + mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context); + if (hasAtLeastOneUninitializedMainDictionary()) { + asyncReloadUninitializedMainDictionaries(context, newLocales, listener); + } + } + if (listener != null) { + listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary()); + } + + // Clean up old dictionaries. + for (final Locale localeToCleanUp : existingDictionariesToCleanup.keySet()) { + final ArrayList<String> dictTypesToCleanUp = + existingDictionariesToCleanup.get(localeToCleanUp); + final DictionaryGroup dictionarySetToCleanup = + findDictionaryGroupWithLocale(oldDictionaryGroups, localeToCleanUp); + for (final String dictType : dictTypesToCleanUp) { + dictionarySetToCleanup.closeDict(dictType); + } + } + } + + private void asyncReloadUninitializedMainDictionaries(final Context context, + final Locale[] locales, final DictionaryInitializationListener listener) { + final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); + mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary; + ExecutorUtils.getExecutor("InitializeBinaryDictionary").execute(new Runnable() { + @Override + public void run() { + doReloadUninitializedMainDictionaries( + context, locales, listener, latchForWaitingLoadingMainDictionary); + } + }); + } + + void doReloadUninitializedMainDictionaries(final Context context, final Locale[] locales, + final DictionaryInitializationListener listener, + final CountDownLatch latchForWaitingLoadingMainDictionary) { + for (final Locale locale : locales) { + final DictionaryGroup dictionaryGroup = + findDictionaryGroupWithLocale(mDictionaryGroups, locale); + if (null == dictionaryGroup) { + // This should never happen, but better safe than crashy + Log.w(TAG, "Expected a dictionary group for " + locale + " but none found"); + continue; + } + final Dictionary mainDict = + DictionaryFactory.createMainDictionaryFromManager(context, locale); + synchronized (mLock) { + if (locale.equals(dictionaryGroup.mLocale)) { + dictionaryGroup.setMainDict(mainDict); + } else { + // Dictionary facilitator has been reset for another locale. + mainDict.close(); + } + } + } + if (listener != null) { + listener.onUpdateMainDictionaryAvailability( + hasAtLeastOneInitializedMainDictionary()); + } + latchForWaitingLoadingMainDictionary.countDown(); + } + + @UsedForTesting + public void resetDictionariesForTesting(final Context context, final Locale[] locales, + final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles, + final Map<String, Map<String, String>> additionalDictAttributes, + @Nullable final String account) { + Dictionary mainDictionary = null; + final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); + + final DictionaryGroup[] dictionaryGroups = new DictionaryGroup[locales.length]; + for (int i = 0; i < locales.length; ++i) { + final Locale locale = locales[i]; + for (final String dictType : dictionaryTypes) { + if (dictType.equals(Dictionary.TYPE_MAIN)) { + mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context, + locale); + } else { + final File dictFile = dictionaryFiles.get(dictType); + final ExpandableBinaryDictionary dict = getSubDict( + dictType, context, locale, dictFile, "" /* dictNamePrefix */, account); + if (additionalDictAttributes.containsKey(dictType)) { + dict.clearAndFlushDictionaryWithAdditionalAttributes( + additionalDictAttributes.get(dictType)); + } + if (dict == null) { + throw new RuntimeException("Unknown dictionary type: " + dictType); + } + dict.reloadDictionaryIfRequired(); + dict.waitAllTasksForTests(); + subDicts.put(dictType, dict); + } + } + dictionaryGroups[i] = new DictionaryGroup(locale, mainDictionary, account, subDicts); + } + mDictionaryGroups = dictionaryGroups; + mMostProbableDictionaryGroup = dictionaryGroups[0]; + } + + public void closeDictionaries() { + final DictionaryGroup[] dictionaryGroups; + synchronized (mLock) { + dictionaryGroups = mDictionaryGroups; + mMostProbableDictionaryGroup = new DictionaryGroup(); + mDictionaryGroups = new DictionaryGroup[] { mMostProbableDictionaryGroup }; + } + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + dictionaryGroup.closeDict(dictType); + } + } + mDistracterFilter.close(); + if (mPersonalizationHelper != null) { + mPersonalizationHelper.close(); + } + } + + @UsedForTesting + public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { + return mMostProbableDictionaryGroup.getSubDict(dictName); + } + + // The main dictionaries are loaded asynchronously. Don't cache the return value + // of these methods. + public boolean hasAtLeastOneInitializedMainDictionary() { + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN); + if (mainDict != null && mainDict.isInitialized()) { + return true; + } + } + return false; + } + + public boolean hasAtLeastOneUninitializedMainDictionary() { + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN); + if (mainDict == null || !mainDict.isInitialized()) { + return true; + } + } + return false; + } + + public boolean hasPersonalizationDictionary() { + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + if (dictionaryGroup.hasDict(Dictionary.TYPE_PERSONALIZATION, null /* account */)) { + return true; + } + } + return false; + } + + public void flushPersonalizationDictionary() { + final HashSet<ExpandableBinaryDictionary> personalizationDictsUsedForSuggestion = + new HashSet<>(); + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + final ExpandableBinaryDictionary personalizationDictUsedForSuggestion = + dictionaryGroup.getSubDict(Dictionary.TYPE_PERSONALIZATION); + personalizationDictsUsedForSuggestion.add(personalizationDictUsedForSuggestion); + } + mPersonalizationHelper.flushPersonalizationDictionariesToUpdate( + personalizationDictsUsedForSuggestion); + mDistracterFilter.close(); + } + + public void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit) + throws InterruptedException { + mLatchForWaitingLoadingMainDictionaries.await(timeout, unit); + } + + @UsedForTesting + public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) + throws InterruptedException { + waitForLoadingMainDictionaries(timeout, unit); + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + for (final ExpandableBinaryDictionary dict : dictionaryGroup.mSubDictMap.values()) { + dict.waitAllTasksForTests(); + } + } + } + + public boolean isUserDictionaryEnabled() { + return mIsUserDictEnabled; + } + + public void addWordToUserDictionary(final Context context, final String word) { + final Locale locale = getMostProbableLocale(); + if (locale == null) { + return; + } + // TODO: add a toast telling what language this is being added to? + UserBinaryDictionary.addWordToUserDictionary(context, locale, word); + } + + public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, + @Nonnull final NgramContext ngramContext, final int timeStampInSeconds, + final boolean blockPotentiallyOffensive) { + final DictionaryGroup dictionaryGroup = getDictionaryGroupForMostProbableLanguage(); + final String[] words = suggestion.split(Constants.WORD_SEPARATOR); + NgramContext ngramContextForCurrentWord = ngramContext; + for (int i = 0; i < words.length; i++) { + final String currentWord = words[i]; + final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false; + addWordToUserHistory(dictionaryGroup, ngramContextForCurrentWord, currentWord, + wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive); + ngramContextForCurrentWord = + ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord)); + } + } + + private void addWordToUserHistory(final DictionaryGroup dictionaryGroup, + final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized, + final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { + final ExpandableBinaryDictionary userHistoryDictionary = + dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY); + if (userHistoryDictionary == null + || !isConfidentAboutCurrentLanguageBeing(userHistoryDictionary.mLocale)) { + return; + } + final int maxFreq = getFrequency(word); + if (maxFreq == 0 && blockPotentiallyOffensive) { + return; + } + final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); + final String secondWord; + if (wasAutoCapitalized) { + if (isValidWord(word, false /* ignoreCase */) + && !isValidWord(lowerCasedWord, false /* ignoreCase */)) { + // 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. + secondWord = word; + } 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. + secondWord = lowerCasedWord; + } + } 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. + final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN, + null /* account */) ? + dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) : + Dictionary.NOT_A_PROBABILITY; + if (maxFreq < lowerCaseFreqInMainDict + && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) { + // Use lower cased word as the word can be a distracter of the popular word. + secondWord = lowerCasedWord; + } else { + secondWord = word; + } + } + // 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 boolean isValid = maxFreq > 0; + UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, secondWord, + isValid, timeStampInSeconds, + new DistracterFilterCheckingIsInDictionary( + mDistracterFilter, userHistoryDictionary)); + } + + private void removeWord(final String dictName, final String word) { + final ExpandableBinaryDictionary dictionary = + getDictionaryGroupForMostProbableLanguage().getSubDict(dictName); + if (dictionary != null) { + dictionary.removeUnigramEntryDynamically(word); + } + } + + public void removeWordFromPersonalizedDicts(final String word) { + removeWord(Dictionary.TYPE_USER_HISTORY, word); + removeWord(Dictionary.TYPE_PERSONALIZATION, word); + removeWord(Dictionary.TYPE_CONTEXTUAL, word); + } + + // TODO: Revise the way to fusion suggestion results. + public SuggestionResults getSuggestionResults(final WordComposer composer, + final NgramContext ngramContext, final long proximityInfoHandle, + final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) { + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + final SuggestionResults suggestionResults = new SuggestionResults( + SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext()); + final float[] weightOfLangModelVsSpatialModel = + new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL }; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + final Dictionary dictionary = dictionaryGroup.getDict(dictType); + if (null == dictionary) continue; + final float weightForLocale = composer.isBatchMode() + ? dictionaryGroup.mWeightForGesturingInLocale + : dictionaryGroup.mWeightForTypingInLocale; + final ArrayList<SuggestedWordInfo> dictionarySuggestions = + dictionary.getSuggestions(composer.getComposedDataSnapshot(), ngramContext, + proximityInfoHandle, settingsValuesForSuggestion, sessionId, + weightForLocale, weightOfLangModelVsSpatialModel); + if (null == dictionarySuggestions) continue; + suggestionResults.addAll(dictionarySuggestions); + if (null != suggestionResults.mRawSuggestions) { + suggestionResults.mRawSuggestions.addAll(dictionarySuggestions); + } + } + } + return suggestionResults; + } + + public boolean isValidWord(final String word, final boolean ignoreCase) { + if (TextUtils.isEmpty(word)) { + return false; + } + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + if (dictionaryGroup.mLocale == null) { + continue; + } + final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + final Dictionary dictionary = dictionaryGroup.getDict(dictType); + // 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 getFrequencyInternal(final String word, + final boolean isGettingMaxFrequencyOfExactMatches) { + if (TextUtils.isEmpty(word)) { + return Dictionary.NOT_A_PROBABILITY; + } + int maxFreq = Dictionary.NOT_A_PROBABILITY; + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + final Dictionary dictionary = dictionaryGroup.getDict(dictType); + if (dictionary == null) continue; + final int tempFreq; + if (isGettingMaxFrequencyOfExactMatches) { + tempFreq = dictionary.getMaxFrequencyOfExactMatches(word); + } else { + tempFreq = dictionary.getFrequency(word); + } + if (tempFreq >= maxFreq) { + maxFreq = tempFreq; + } + } + } + return maxFreq; + } + + public int getFrequency(final String word) { + return getFrequencyInternal(word, false /* isGettingMaxFrequencyOfExactMatches */); + } + + public int getMaxFrequencyOfExactMatches(final String word) { + return getFrequencyInternal(word, true /* isGettingMaxFrequencyOfExactMatches */); + } + + private void clearSubDictionary(final String dictName) { + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictName); + if (dictionary != null) { + dictionary.clear(); + } + } + } + + public void clearUserHistoryDictionary() { + clearSubDictionary(Dictionary.TYPE_USER_HISTORY); + } + + // This method gets called only when the IME receives a notification to remove the + // personalization dictionary. + public void clearPersonalizationDictionary() { + clearSubDictionary(Dictionary.TYPE_PERSONALIZATION); + mPersonalizationHelper.clearDictionariesToUpdate(); + } + + public void clearContextualDictionary() { + clearSubDictionary(Dictionary.TYPE_CONTEXTUAL); + } + + public void addEntriesToPersonalizationDictionary( + final PersonalizationDataChunk personalizationDataChunk, + final SpacingAndPunctuations spacingAndPunctuations, + final UpdateEntriesForInputEventsCallback callback) { + mPersonalizationHelper.updateEntriesOfPersonalizationDictionaries( + getMostProbableLocale(), personalizationDataChunk, spacingAndPunctuations, + callback); + } + + @UsedForTesting + public void addPhraseToContextualDictionary(final String[] phrase, final int probability, + final int bigramProbabilityForWords, final int bigramProbabilityForPhrases) { + // TODO: we're inserting the phrase into the dictionary for the active language. Rethink + // this a bit from a theoretical point of view. + final ExpandableBinaryDictionary contextualDict = + getDictionaryGroupForMostProbableLanguage().getSubDict(Dictionary.TYPE_CONTEXTUAL); + if (contextualDict == null) { + return; + } + NgramContext ngramContext = NgramContext.BEGINNING_OF_SENTENCE; + for (int i = 0; i < phrase.length; i++) { + final String[] subPhrase = Arrays.copyOfRange(phrase, i /* start */, phrase.length); + final String subPhraseStr = TextUtils.join(Constants.WORD_SEPARATOR, subPhrase); + contextualDict.addUnigramEntryWithCheckingDistracter( + subPhraseStr, probability, null /* shortcutTarget */, + Dictionary.NOT_A_PROBABILITY /* shortcutFreq */, + false /* isNotAWord */, false /* isPossiblyOffensive */, + BinaryDictionary.NOT_A_VALID_TIMESTAMP, + DistracterFilter.EMPTY_DISTRACTER_FILTER); + contextualDict.addNgramEntry(ngramContext, subPhraseStr, + bigramProbabilityForPhrases, BinaryDictionary.NOT_A_VALID_TIMESTAMP); + + if (i < phrase.length - 1) { + contextualDict.addUnigramEntryWithCheckingDistracter( + phrase[i], probability, null /* shortcutTarget */, + Dictionary.NOT_A_PROBABILITY /* shortcutFreq */, + false /* isNotAWord */, false /* isPossiblyOffensive */, + BinaryDictionary.NOT_A_VALID_TIMESTAMP, + DistracterFilter.EMPTY_DISTRACTER_FILTER); + contextualDict.addNgramEntry(ngramContext, phrase[i], + bigramProbabilityForWords, BinaryDictionary.NOT_A_VALID_TIMESTAMP); + } + ngramContext = + ngramContext.getNextNgramContext(new NgramContext.WordInfo(phrase[i])); + } + } + + public void dumpDictionaryForDebug(final String dictName) { + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + final ExpandableBinaryDictionary dictToDump = dictionaryGroup.getSubDict(dictName); + if (dictToDump == null) { + Log.e(TAG, "Cannot dump " + dictName + ". " + + "The dictionary is not being used for suggestion or cannot be dumped."); + return; + } + dictToDump.dumpAllWordsForDebug(); + } + } + + public ArrayList<Pair<String, DictionaryStats>> getStatsOfEnabledSubDicts() { + final ArrayList<Pair<String, DictionaryStats>> statsOfEnabledSubDicts = new ArrayList<>(); + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + for (final String dictType : SUB_DICT_TYPES) { + final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictType); + if (dictionary == null) continue; + statsOfEnabledSubDicts.add(new Pair<>(dictType, dictionary.getDictionaryStats())); + } + } + return statsOfEnabledSubDicts; + } +} diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java index 3119ff82f..13bd15101 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java @@ -136,7 +136,7 @@ public class DictionaryFacilitatorLruCache { if (dictionaryFacilitator != null) { return dictionaryFacilitator; } - dictionaryFacilitator = new DictionaryFacilitator(); + dictionaryFacilitator = DictionaryFacilitatorProvider.newDictionaryFacilitator(); resetDictionariesForLocaleLocked(dictionaryFacilitator, locale); waitForLoadingMainDictionary(dictionaryFacilitator); mLruCache.put(locale, dictionaryFacilitator); diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 2e6541ff1..622cdb0a6 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -139,8 +139,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private static final String SCHEME_PACKAGE = "package"; final Settings mSettings; - private final DictionaryFacilitator mDictionaryFacilitator = - new DictionaryFacilitator(this /* context */); + private final DictionaryFacilitator mDictionaryFacilitator = + DictionaryFacilitatorProvider.newDictionaryFacilitator(this /* context */); // TODO: Move from LatinIME. private final PersonalizationDictionaryUpdater mPersonalizationDictionaryUpdater = new PersonalizationDictionaryUpdater(this /* context */, mDictionaryFacilitator); diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java index 811af4bd7..64a7cf347 100644 --- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java +++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java @@ -33,7 +33,6 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; -import com.android.inputmethod.latin.settings.AdditionalFeaturesSettingUtils; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; import com.android.inputmethod.latin.utils.LanguageOnSpacebarUtils; @@ -324,22 +323,6 @@ public class RichInputMethodManager { return INDEX_NOT_FOUND; } - public boolean checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype) { - return getSubtypeIndexInIme(subtype, getInputMethodInfoOfThisIme()) != INDEX_NOT_FOUND; - } - - private static int getSubtypeIndexInIme(final InputMethodSubtype subtype, - final InputMethodInfo imi) { - final int count = imi.getSubtypeCount(); - for (int index = 0; index < count; index++) { - final InputMethodSubtype ims = imi.getSubtypeAt(index); - if (ims.equals(subtype)) { - return index; - } - } - return INDEX_NOT_FOUND; - } - public void onSubtypeChanged(@Nonnull final InputMethodSubtype newSubtype) { updateCurrentSubtype(newSubtype); updateShortcutIme(); @@ -416,7 +399,6 @@ public class RichInputMethodManager { // subtypes should be counted as well. if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) { ++filteredImisCount; - continue; } } diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index e605bfe78..e80e3628f 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -103,8 +103,7 @@ public final class WordComposer { final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec; if (!nonNullCombiningSpec.equals(mCombiningSpec)) { mCombinerChain = new CombinerChain( - mCombinerChain.getComposingWordWithCombiningFeedback().toString(), - CombinerChain.createCombiners(nonNullCombiningSpec)); + mCombinerChain.getComposingWordWithCombiningFeedback().toString()); mCombiningSpec = nonNullCombiningSpec; } } diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 0185a04ef..4842438c8 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -2204,8 +2204,7 @@ public final class InputLogic { mWordComposer.isComposingWord() ? 2 : 1), proximityInfo, new SettingsValuesForSuggestion(settingsValues.mBlockPotentiallyOffensive, - settingsValues.mPhraseGestureEnabled, - settingsValues.mAdditionalFeaturesSettingValues), + settingsValues.mPhraseGestureEnabled), settingsValues.mAutoCorrectionEnabledPerUserSettings, inputStyle, sequenceNumber, callback); } diff --git a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java index 3dfc743f8..9366726e2 100644 --- a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java @@ -103,8 +103,6 @@ public final class AdvancedSettingsFragment extends SubScreenFragment { removePreference(Settings.PREF_ENABLE_METRICS_LOGGING); } - AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(context, this); - setupKeypressVibrationDurationSettings(); setupKeypressSoundVolumeSettings(); setupKeyLongpressTimeoutSettings(); diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java index b788d7fcf..a56de1f69 100644 --- a/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java @@ -28,7 +28,7 @@ import android.preference.PreferenceGroup; import android.preference.TwoStatePreference; import com.android.inputmethod.latin.DictionaryDumpBroadcastReceiver; -import com.android.inputmethod.latin.DictionaryFacilitator; +import com.android.inputmethod.latin.DictionaryFacilitatorImpl; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.debug.ExternalDictionaryGetterForDebug; import com.android.inputmethod.latin.utils.ApplicationUtils; @@ -67,7 +67,7 @@ public final class DebugSettingsFragment extends SubScreenFragment final PreferenceGroup dictDumpPreferenceGroup = (PreferenceGroup)findPreference(PREF_KEY_DUMP_DICTS); - for (final String dictName : DictionaryFacilitator.DICT_TYPE_TO_CLASS.keySet()) { + for (final String dictName : DictionaryFacilitatorImpl.DICT_TYPE_TO_CLASS.keySet()) { final Preference pref = new DictDumpPreference(getActivity(), dictName); pref.setOnPreferenceClickListener(this); dictDumpPreferenceGroup.addPreference(pref); diff --git a/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java deleted file mode 100644 index c5930db1e..000000000 --- a/java/src/com/android/inputmethod/latin/settings/MultiLingualSettingsFragment.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.settings; - -import android.os.Bundle; - -import com.android.inputmethod.latin.R; - -/** - * "Multilingual options" settings sub screen. - * - * This settings sub screen handles the following input preferences. - * - Language switch key - * - Switch to other input methods - */ -public final class MultiLingualSettingsFragment extends SubScreenFragment { - @Override - public void onCreate(final Bundle icicle) { - super.onCreate(icicle); - addPreferencesFromResource(R.xml.prefs_screen_multilingual); - if (!Settings.ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS) { - removePreference(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY); - removePreference(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST); - } - AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(getActivity(), this); - } -} diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index 77996405b..0ac19f76b 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -44,14 +44,8 @@ import javax.annotation.Nonnull; public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = Settings.class.getSimpleName(); // Settings screens - public static final String SCREEN_PREFERENCES = "screen_preferences"; public static final String SCREEN_ACCOUNTS = "screen_accounts"; - public static final String SCREEN_APPEARANCE = "screen_appearance"; public static final String SCREEN_THEME = "screen_theme"; - public static final String SCREEN_MULTILINGUAL = "screen_multilingual"; - public static final String SCREEN_GESTURE = "screen_gesture"; - public static final String SCREEN_CORRECTION = "screen_correction"; - public static final String SCREEN_ADVANCED = "screen_advanced"; public static final String SCREEN_DEBUG = "screen_debug"; // In the same order as xml/prefs.xml public static final String PREF_AUTO_CAP = "auto_cap"; @@ -73,9 +67,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang "pref_key_use_double_space_period"; public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE = "pref_key_block_potentially_offensive"; - // No multilingual options in Android L and above for now. - public static final boolean SHOW_MULTILINGUAL_SETTINGS = - BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.KITKAT; public static final boolean ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS = BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.KITKAT; public static final boolean SHOULD_SHOW_LXX_SUGGESTION_UI = @@ -84,7 +75,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang "pref_show_language_switch_key"; public static final String PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST = "pref_include_other_imes_in_language_switch_list"; - public static final String PREF_KEYBOARD_THEME = "pref_keyboard_theme"; public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles"; public static final String PREF_ENABLE_SPLIT_KEYBOARD = "pref_split_keyboard"; // TODO: consolidate key preview dismiss delay with the key preview animation parameters. @@ -103,8 +93,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang "pref_gesture_floating_preview_text"; public static final String PREF_PHRASE_GESTURE_ENABLED = "pref_gesture_space_aware"; - public static final String PREF_INPUT_LANGUAGE = "input_language"; - public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; public static final String PREF_KEY_IS_INTERNAL = "pref_key_is_internal"; public static final String PREF_ENABLE_METRICS_LOGGING = "pref_enable_metrics_logging"; @@ -203,14 +191,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return mSettingsValues.mIsInternal; } - public boolean isWordSeparator(final int code) { - return mSettingsValues.isWordSeparator(code); - } - - public boolean getBlockPotentiallyOffensive() { - return mSettingsValues.mBlockPotentiallyOffensive; - } - public static int readScreenMetrics(final Resources res) { return res.getInteger(R.integer.config_screen_metrics); } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java index 6c21accf6..b98c53af4 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java @@ -48,15 +48,10 @@ public final class SettingsFragment extends InputMethodSettingsFragment { final PreferenceScreen preferenceScreen = getPreferenceScreen(); preferenceScreen.setTitle( ApplicationUtils.getActivityTitleResId(getActivity(), SettingsActivity.class)); - if (!Settings.SHOW_MULTILINGUAL_SETTINGS) { - final Preference multilingualOptions = findPreference(Settings.SCREEN_MULTILINGUAL); - preferenceScreen.removePreference(multilingualOptions); - } if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) { final Preference accountsPreference = findPreference(Settings.SCREEN_ACCOUNTS); preferenceScreen.removePreference(accountsPreference); } - AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(getActivity(), this); } @Override diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index 0669026d8..a080515dd 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -102,10 +102,6 @@ public class SettingsValues { private final boolean mSuggestionsEnabledPerUserSettings; private final AsyncResultHolder<AppWorkaroundsUtils> mAppWorkarounds; - // Setting values for additional features - public final int[] mAdditionalFeaturesSettingValues = - new int[AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE]; - // TextDecorator public final int mTextHighlightColorForAddToDictionaryIndicator; @@ -187,8 +183,6 @@ public class SettingsValues { mAutoCorrectionEnabledPerUserSettings = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; mSuggestionsEnabledPerUserSettings = readSuggestionsEnabled(prefs); - AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray(context, - prefs, mAdditionalFeaturesSettingValues); mTextHighlightColorForAddToDictionaryIndicator = res.getColor( R.color.text_decorator_add_to_dictionary_indicator_text_highlight_color); mIsInternal = Settings.isInternal(prefs); @@ -437,8 +431,6 @@ public class SettingsValues { sb.append("\n mAppWorkarounds = "); final AppWorkaroundsUtils awu = mAppWorkarounds.get(null, 0); sb.append("" + (null == awu ? "null" : awu.toString())); - sb.append("\n mAdditionalFeaturesSettingValues = "); - sb.append("" + Arrays.toString(mAdditionalFeaturesSettingValues)); sb.append("\n mTextHighlightColorForAddToDictionaryIndicator = "); sb.append("" + mTextHighlightColorForAddToDictionaryIndicator); sb.append("\n mIsInternal = "); diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValuesForSuggestion.java b/java/src/com/android/inputmethod/latin/settings/SettingsValuesForSuggestion.java index d80af4ba7..56e6fac71 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValuesForSuggestion.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValuesForSuggestion.java @@ -19,12 +19,10 @@ package com.android.inputmethod.latin.settings; public class SettingsValuesForSuggestion { public final boolean mBlockPotentiallyOffensive; public final boolean mSpaceAwareGestureEnabled; - public final int[] mAdditionalFeaturesSettingValues; public SettingsValuesForSuggestion(final boolean blockPotentiallyOffensive, - final boolean spaceAwareGestureEnabled, final int[] additionalFeaturesSettingValues) { + final boolean spaceAwareGestureEnabled) { mBlockPotentiallyOffensive = blockPotentiallyOffensive; mSpaceAwareGestureEnabled = spaceAwareGestureEnabled; - mAdditionalFeaturesSettingValues = additionalFeaturesSettingValues; } } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index bcf7bbfdc..8744020b1 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -76,8 +76,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService // TODO: make a spell checker option to block offensive words or not private final SettingsValuesForSuggestion mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */, - true /* spaceAwareGestureEnabled */, - null /* additionalFeaturesSettingValues */); + true /* spaceAwareGestureEnabled */); public static final String SINGLE_QUOTE = "\u0027"; public static final String APOSTROPHE = "\u2019"; diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index 4b8d2a3f9..832bfd066 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -39,6 +39,7 @@ import com.android.inputmethod.latin.common.LocaleUtils; import com.android.inputmethod.latin.common.StringUtils; import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; import com.android.inputmethod.latin.utils.ScriptUtils; +import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.SuggestionResults; import java.util.ArrayList; @@ -297,6 +298,15 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { } } } + // Handle word not in dictionary. + // This is called only once per unique word, so entering multiple + // instances of the same word does not result in more than one call + // to this method. + // Also, upon changing the orientation of the device, this is called + // again for every unique invalid word in the text box. + if (!isInDict) { + StatsUtils.onInvalidWordIdentification(text); + } final int flags = (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java index 9c6a94810..56a04a856 100644 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java @@ -238,8 +238,7 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr } final SettingsValuesForSuggestion settingsValuesForSuggestion = new SettingsValuesForSuggestion(false /* blockPotentiallyOffensive */, - false /* spaceAwareGestureEnabled */, - null /* additionalFeaturesSettingValues */); + false /* spaceAwareGestureEnabled */); final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(testedWord); final String consideredWord = trailingSingleQuotesCount > 0 ? testedWord.substring(0, testedWord.length() - trailingSingleQuotesCount) : diff --git a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java index ae2de44c7..147e57b13 100644 --- a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java @@ -25,7 +25,6 @@ import com.android.inputmethod.latin.settings.CorrectionSettingsFragment; import com.android.inputmethod.latin.settings.CustomInputStyleSettingsFragment; import com.android.inputmethod.latin.settings.DebugSettingsFragment; import com.android.inputmethod.latin.settings.GestureSettingsFragment; -import com.android.inputmethod.latin.settings.MultiLingualSettingsFragment; import com.android.inputmethod.latin.settings.PreferencesSettingsFragment; import com.android.inputmethod.latin.settings.SettingsFragment; import com.android.inputmethod.latin.settings.ThemeSettingsFragment; @@ -46,7 +45,6 @@ public class FragmentUtils { sLatinImeFragments.add(AccountsSettingsFragment.class.getName()); sLatinImeFragments.add(AppearanceSettingsFragment.class.getName()); sLatinImeFragments.add(ThemeSettingsFragment.class.getName()); - sLatinImeFragments.add(MultiLingualSettingsFragment.class.getName()); sLatinImeFragments.add(CustomInputStyleSettingsFragment.class.getName()); sLatinImeFragments.add(GestureSettingsFragment.class.getName()); sLatinImeFragments.add(CorrectionSettingsFragment.class.getName()); |