mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-05-04 13:50:42 +00:00
add multilingual typing
This commit is contained in:
parent
0f585f9f1b
commit
ac7ac4f907
9 changed files with 504 additions and 35 deletions
|
@ -11,7 +11,9 @@ Plan / to do:
|
||||||
* maybe: rename (package, app, icon), so it can be installed parallel to OpenBoard, and published on F-Droid
|
* maybe: rename (package, app, icon), so it can be installed parallel to OpenBoard, and published on F-Droid
|
||||||
* ~user-selectable dictionaries, https://github.com/openboard-team/openboard/pull/578~
|
* ~user-selectable dictionaries, https://github.com/openboard-team/openboard/pull/578~
|
||||||
* ~make additional dictionaries available for download (from OpenBoard PRs)~
|
* ~make additional dictionaries available for download (from OpenBoard PRs)~
|
||||||
* multi-lingual typing, https://github.com/openboard-team/openboard/pull/593
|
* ~multi-lingual typing, https://github.com/openboard-team/openboard/pull/586, https://github.com/openboard-team/openboard/pull/593~
|
||||||
|
* maybe improve way of merging suggestions from both languages
|
||||||
|
* test whether it works reasonably well in non-latin scripts
|
||||||
* ~suggestion fixes, https://github.com/openboard-team/openboard/pull/694, https://github.com/openboard-team/openboard/issues/795, https://github.com/openboard-team/openboard/issues/660~
|
* ~suggestion fixes, https://github.com/openboard-team/openboard/pull/694, https://github.com/openboard-team/openboard/issues/795, https://github.com/openboard-team/openboard/issues/660~
|
||||||
* ~improve auto-space insertion, https://github.com/openboard-team/openboard/pull/576~
|
* ~improve auto-space insertion, https://github.com/openboard-team/openboard/pull/576~
|
||||||
* emoji prediction/search, either https://github.com/openboard-team/openboard/pull/749 or use dictionaries
|
* emoji prediction/search, either https://github.com/openboard-team/openboard/pull/749 or use dictionaries
|
||||||
|
@ -19,6 +21,7 @@ Plan / to do:
|
||||||
* delete suggestions, https://github.com/openboard-team/openboard/issues/106
|
* delete suggestions, https://github.com/openboard-team/openboard/issues/106
|
||||||
* glide typing, https://github.com/openboard-team/openboard/issues/3
|
* glide typing, https://github.com/openboard-team/openboard/issues/3
|
||||||
* license issues, maybe allow using an external library
|
* license issues, maybe allow using an external library
|
||||||
|
* re-consider preferring lowercase word for typed history in some cases (DictionaryFacilitatorImpl.addWordToUserHistory)
|
||||||
|
|
||||||
Changes:
|
Changes:
|
||||||
* Updated dependencies
|
* Updated dependencies
|
||||||
|
@ -34,6 +37,7 @@ Changes:
|
||||||
* Fix suggestions sometimes not being shown, https://github.com/openboard-team/openboard/pull/709
|
* Fix suggestions sometimes not being shown, https://github.com/openboard-team/openboard/pull/709
|
||||||
* Reduce amount of unwanted automatic space insertions, https://github.com/openboard-team/openboard/pull/576
|
* Reduce amount of unwanted automatic space insertions, https://github.com/openboard-team/openboard/pull/576
|
||||||
* Exit shift-symbol view on space (same as normal symbol view)
|
* Exit shift-symbol view on space (same as normal symbol view)
|
||||||
|
* Add multi-lingual typing, slightly modified from https://github.com/openboard-team/openboard/pull/586, https://github.com/openboard-team/openboard/pull/593
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
@ -55,10 +55,12 @@ import org.dslul.openboard.inputmethod.latin.SuggestedWords;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
|
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.DebugSettings;
|
import org.dslul.openboard.inputmethod.latin.settings.DebugSettings;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.settings.Settings;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.LanguageOnSpacebarUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.LanguageOnSpacebarUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.TypefaceUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.TypefaceUtils;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
@ -838,6 +840,25 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
||||||
private String layoutLanguageOnSpacebar(final Paint paint,
|
private String layoutLanguageOnSpacebar(final Paint paint,
|
||||||
final RichInputMethodSubtype subtype, final int width) {
|
final RichInputMethodSubtype subtype, final int width) {
|
||||||
// Choose appropriate language name to fit into the width.
|
// Choose appropriate language name to fit into the width.
|
||||||
|
|
||||||
|
final Locale secondaryLocale = Settings.getInstance().getCurrent().mSecondaryLocale;
|
||||||
|
if (secondaryLocale != null
|
||||||
|
// avoid showing same language twice
|
||||||
|
&& !secondaryLocale.getLanguage().equals(subtype.getLocale().getLanguage())
|
||||||
|
) {
|
||||||
|
final Locale displayLocale = getResources().getConfiguration().locale;
|
||||||
|
final String full = subtype.getMiddleDisplayName() + " - " +
|
||||||
|
secondaryLocale.getDisplayLanguage(displayLocale);
|
||||||
|
if (fitsTextIntoWidth(width, full, paint)) {
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
final String middle = subtype.getLocale().getLanguage().toUpperCase(displayLocale) +
|
||||||
|
" - " + secondaryLocale.getLanguage().toUpperCase(displayLocale);
|
||||||
|
if (fitsTextIntoWidth(width, middle, paint)) {
|
||||||
|
return middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mLanguageOnSpacebarFormatType == LanguageOnSpacebarUtils.FORMAT_TYPE_FULL_LOCALE) {
|
if (mLanguageOnSpacebarFormatType == LanguageOnSpacebarUtils.FORMAT_TYPE_FULL_LOCALE) {
|
||||||
final String fullText = subtype.getFullDisplayName();
|
final String fullText = subtype.getFullDisplayName();
|
||||||
if (fitsTextIntoWidth(width, fullText, paint)) {
|
if (fitsTextIntoWidth(width, fullText, paint)) {
|
||||||
|
|
|
@ -31,8 +31,10 @@ import org.dslul.openboard.inputmethod.latin.common.Constants;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.permissions.PermissionsUtil;
|
import org.dslul.openboard.inputmethod.latin.permissions.PermissionsUtil;
|
||||||
import org.dslul.openboard.inputmethod.latin.personalization.UserHistoryDictionary;
|
import org.dslul.openboard.inputmethod.latin.personalization.UserHistoryDictionary;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.settings.Settings;
|
||||||
import org.dslul.openboard.inputmethod.latin.settings.SettingsValuesForSuggestion;
|
import org.dslul.openboard.inputmethod.latin.settings.SettingsValuesForSuggestion;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.ExecutorUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.ExecutorUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.SuggestionResults;
|
import org.dslul.openboard.inputmethod.latin.utils.SuggestionResults;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -69,6 +71,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140;
|
private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140;
|
||||||
|
|
||||||
private DictionaryGroup mDictionaryGroup = new DictionaryGroup();
|
private DictionaryGroup mDictionaryGroup = new DictionaryGroup();
|
||||||
|
private DictionaryGroup mSecondaryDictionaryGroup = new DictionaryGroup();
|
||||||
private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0);
|
private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0);
|
||||||
// To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
|
// To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
|
||||||
private final Object mLock = new Object();
|
private final Object mLock = new Object();
|
||||||
|
@ -86,6 +89,9 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES =
|
private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES =
|
||||||
new Class[] { Context.class, Locale.class, File.class, String.class, String.class };
|
new Class[] { Context.class, Locale.class, File.class, String.class, String.class };
|
||||||
|
|
||||||
|
// these caches are never even set, as the corresponding functions are not called...
|
||||||
|
// and even if they were set, one is only written, but never read, and the other one
|
||||||
|
// is only read and thus empty and useless
|
||||||
private LruCache<String, Boolean> mValidSpellingWordReadCache;
|
private LruCache<String, Boolean> mValidSpellingWordReadCache;
|
||||||
private LruCache<String, Boolean> mValidSpellingWordWriteCache;
|
private LruCache<String, Boolean> mValidSpellingWordWriteCache;
|
||||||
|
|
||||||
|
@ -104,6 +110,11 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
return locale != null && locale.equals(mDictionaryGroup.mLocale);
|
return locale != null && locale.equals(mDictionaryGroup.mLocale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasLocale(final Locale locale) {
|
||||||
|
return locale != null && (locale.equals(mDictionaryGroup.mLocale) ||
|
||||||
|
(mSecondaryDictionaryGroup != null && locale.equals(mSecondaryDictionaryGroup.mLocale)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether this facilitator is exactly for this account.
|
* Returns whether this facilitator is exactly for this account.
|
||||||
*
|
*
|
||||||
|
@ -124,6 +135,9 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
public static final float WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.95f;
|
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;
|
public static final float WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.6f;
|
||||||
|
|
||||||
|
private static final int MAX_CONFIDENCE = 2;
|
||||||
|
private static final int MIN_CONFIDENCE = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The locale associated with the dictionary group.
|
* The locale associated with the dictionary group.
|
||||||
*/
|
*/
|
||||||
|
@ -137,8 +151,34 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
@Nullable private Dictionary mMainDict;
|
@Nullable private Dictionary mMainDict;
|
||||||
// Confidence that the most probable language is actually the language the user is
|
// 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
|
// typing in. For now, this is simply the number of times a word from this language
|
||||||
// has been committed in a row.
|
// has been committed in a row, with an exception when typing a single word not contained
|
||||||
private int mConfidence = 0;
|
// in this language.
|
||||||
|
private int mConfidence = 1;
|
||||||
|
|
||||||
|
// allow to go above max confidence, for better determination of currently preferred language
|
||||||
|
// when decreasing confidence or getting weight factor, limit to maximum
|
||||||
|
public void increaseConfidence() {
|
||||||
|
mConfidence += 1;
|
||||||
|
if (mConfidence <= MAX_CONFIDENCE)
|
||||||
|
updateWeights();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If confidence is above max, drop to max confidence. This does not change weights and
|
||||||
|
// allows conveniently typing single words from the other language without affecting suggestions
|
||||||
|
public void decreaseConfidence() {
|
||||||
|
if (mConfidence > MAX_CONFIDENCE)
|
||||||
|
mConfidence = MAX_CONFIDENCE;
|
||||||
|
else if (mConfidence > MIN_CONFIDENCE) {
|
||||||
|
mConfidence -= 1;
|
||||||
|
updateWeights();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: might need some more tuning, maybe more confidence steps
|
||||||
|
private void updateWeights() {
|
||||||
|
mWeightForTypingInLocale = 1f - 0.15f * (MAX_CONFIDENCE - mConfidence);
|
||||||
|
mWeightForGesturingInLocale = 1f - 0.05f * (MAX_CONFIDENCE - mConfidence);
|
||||||
|
}
|
||||||
|
|
||||||
public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
|
public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
|
||||||
public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
|
public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
|
||||||
|
@ -229,6 +269,11 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
Dictionary dict = mDictionaryGroup.getDict(dictType);
|
Dictionary dict = mDictionaryGroup.getDict(dictType);
|
||||||
if (dict != null) dict.onFinishInput();
|
if (dict != null) dict.onFinishInput();
|
||||||
}
|
}
|
||||||
|
if (mSecondaryDictionaryGroup != null)
|
||||||
|
for (final String dictType : ALL_DICTIONARY_TYPES) {
|
||||||
|
Dictionary dict = mSecondaryDictionaryGroup.getDict(dictType);
|
||||||
|
if (dict != null) dict.onFinishInput();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -276,10 +321,15 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
@Nullable
|
@Nullable
|
||||||
static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup dictionaryGroup,
|
static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup dictionaryGroup,
|
||||||
final Locale locale) {
|
final Locale locale) {
|
||||||
|
if (dictionaryGroup == null) return null;
|
||||||
return locale.equals(dictionaryGroup.mLocale) ? dictionaryGroup : null;
|
return locale.equals(dictionaryGroup.mLocale) ? dictionaryGroup : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
// TODO: what if secondary locale changes, but main remains same?
|
||||||
|
// current reset doesn't consider this (not here, and not in other places where locales
|
||||||
|
// are checked against current locale)
|
||||||
|
// but that actually shouldn't happen anyway...
|
||||||
public void resetDictionaries(
|
public void resetDictionaries(
|
||||||
final Context context,
|
final Context context,
|
||||||
final Locale newLocale,
|
final Locale newLocale,
|
||||||
|
@ -353,13 +403,56 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
DictionaryGroup newDictionaryGroup =
|
DictionaryGroup newDictionaryGroup =
|
||||||
new DictionaryGroup(newLocale, mainDict, account, subDicts);
|
new DictionaryGroup(newLocale, mainDict, account, subDicts);
|
||||||
|
|
||||||
|
// create / load secondary dictionary
|
||||||
|
final Locale secondaryLocale = Settings.getInstance().getCurrent().mSecondaryLocale;
|
||||||
|
final DictionaryGroup newSecondaryDictionaryGroup;
|
||||||
|
final Map<String, ExpandableBinaryDictionary> secondarySubDicts = new HashMap<>();
|
||||||
|
|
||||||
|
if (secondaryLocale != null &&
|
||||||
|
ScriptUtils.getScriptFromSpellCheckerLocale(secondaryLocale) == ScriptUtils.getScriptFromSpellCheckerLocale(newLocale)) {
|
||||||
|
final ArrayList<String> dictTypesToCleanUp = new ArrayList<>();
|
||||||
|
for (final String dictType : ALL_DICTIONARY_TYPES) {
|
||||||
|
if (mSecondaryDictionaryGroup != null && mSecondaryDictionaryGroup.hasDict(dictType, account)) {
|
||||||
|
dictTypesToCleanUp.add(dictType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final String subDictType : subDictTypesToUse) {
|
||||||
|
final ExpandableBinaryDictionary subDict =
|
||||||
|
getSubDict(subDictType, context, secondaryLocale, null, dictNamePrefix, account);
|
||||||
|
secondarySubDicts.put(subDictType, subDict);
|
||||||
|
dictTypesToCleanUp.remove(subDictType);
|
||||||
|
}
|
||||||
|
final Dictionary secondaryMainDict;
|
||||||
|
if (forceReloadMainDictionary || findDictionaryGroupWithLocale(mSecondaryDictionaryGroup, secondaryLocale) == null
|
||||||
|
|| !mSecondaryDictionaryGroup.hasDict(Dictionary.TYPE_MAIN, account)) {
|
||||||
|
secondaryMainDict = null;
|
||||||
|
} else {
|
||||||
|
if (mSecondaryDictionaryGroup == null)
|
||||||
|
secondaryMainDict = null;
|
||||||
|
else
|
||||||
|
secondaryMainDict = mSecondaryDictionaryGroup.getDict(Dictionary.TYPE_MAIN);
|
||||||
|
dictTypesToCleanUp.remove(Dictionary.TYPE_MAIN);
|
||||||
|
}
|
||||||
|
newSecondaryDictionaryGroup = new DictionaryGroup(secondaryLocale, secondaryMainDict, account, secondarySubDicts);
|
||||||
|
|
||||||
|
// do the cleanup like for main dict: look like this is for removing dictionaries
|
||||||
|
// after user changed enabled types (e.g. disable personalized suggestions)
|
||||||
|
existingDictionariesToCleanup.put(secondaryLocale, dictTypesToCleanUp);
|
||||||
|
} else {
|
||||||
|
newSecondaryDictionaryGroup = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Replace Dictionaries.
|
// Replace Dictionaries.
|
||||||
final DictionaryGroup oldDictionaryGroup;
|
final DictionaryGroup oldDictionaryGroup;
|
||||||
|
final DictionaryGroup oldSecondaryDictionaryGroup;
|
||||||
synchronized (mLock) {
|
synchronized (mLock) {
|
||||||
oldDictionaryGroup = mDictionaryGroup;
|
oldDictionaryGroup = mDictionaryGroup;
|
||||||
mDictionaryGroup = newDictionaryGroup;
|
mDictionaryGroup = newDictionaryGroup;
|
||||||
|
oldSecondaryDictionaryGroup = mSecondaryDictionaryGroup;
|
||||||
|
mSecondaryDictionaryGroup = newSecondaryDictionaryGroup;
|
||||||
if (hasAtLeastOneUninitializedMainDictionary()) {
|
if (hasAtLeastOneUninitializedMainDictionary()) {
|
||||||
asyncReloadUninitializedMainDictionaries(context, newLocale, listener);
|
asyncReloadUninitializedMainDictionaries(context, newLocale,
|
||||||
|
mSecondaryDictionaryGroup == null ? null : secondaryLocale, listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
|
@ -370,8 +463,13 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
for (final Locale localeToCleanUp : existingDictionariesToCleanup.keySet()) {
|
for (final Locale localeToCleanUp : existingDictionariesToCleanup.keySet()) {
|
||||||
final ArrayList<String> dictTypesToCleanUp =
|
final ArrayList<String> dictTypesToCleanUp =
|
||||||
existingDictionariesToCleanup.get(localeToCleanUp);
|
existingDictionariesToCleanup.get(localeToCleanUp);
|
||||||
final DictionaryGroup dictionarySetToCleanup =
|
DictionaryGroup dictionarySetToCleanup =
|
||||||
findDictionaryGroupWithLocale(oldDictionaryGroup, localeToCleanUp);
|
findDictionaryGroupWithLocale(oldDictionaryGroup, localeToCleanUp);
|
||||||
|
if (dictionarySetToCleanup == null)
|
||||||
|
dictionarySetToCleanup =
|
||||||
|
findDictionaryGroupWithLocale(oldSecondaryDictionaryGroup, localeToCleanUp);
|
||||||
|
if (dictionarySetToCleanup == null)
|
||||||
|
continue;
|
||||||
for (final String dictType : dictTypesToCleanUp) {
|
for (final String dictType : dictTypesToCleanUp) {
|
||||||
dictionarySetToCleanup.closeDict(dictType);
|
dictionarySetToCleanup.closeDict(dictType);
|
||||||
}
|
}
|
||||||
|
@ -383,20 +481,20 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void asyncReloadUninitializedMainDictionaries(final Context context,
|
private void asyncReloadUninitializedMainDictionaries(final Context context,
|
||||||
final Locale locale, final DictionaryInitializationListener listener) {
|
final Locale locale, final Locale secondaryLocale, final DictionaryInitializationListener listener) {
|
||||||
final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1);
|
final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1);
|
||||||
mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary;
|
mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary;
|
||||||
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
|
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
doReloadUninitializedMainDictionaries(
|
doReloadUninitializedMainDictionaries(
|
||||||
context, locale, listener, latchForWaitingLoadingMainDictionary);
|
context, locale, secondaryLocale, listener, latchForWaitingLoadingMainDictionary);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void doReloadUninitializedMainDictionaries(final Context context, final Locale locale,
|
void doReloadUninitializedMainDictionaries(final Context context, final Locale locale,
|
||||||
final DictionaryInitializationListener listener,
|
final Locale secondaryLocale, final DictionaryInitializationListener listener,
|
||||||
final CountDownLatch latchForWaitingLoadingMainDictionary) {
|
final CountDownLatch latchForWaitingLoadingMainDictionary) {
|
||||||
final DictionaryGroup dictionaryGroup =
|
final DictionaryGroup dictionaryGroup =
|
||||||
findDictionaryGroupWithLocale(mDictionaryGroup, locale);
|
findDictionaryGroupWithLocale(mDictionaryGroup, locale);
|
||||||
|
@ -407,6 +505,18 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
}
|
}
|
||||||
final Dictionary mainDict =
|
final Dictionary mainDict =
|
||||||
DictionaryFactory.createMainDictionaryFromManager(context, locale);
|
DictionaryFactory.createMainDictionaryFromManager(context, locale);
|
||||||
|
|
||||||
|
final DictionaryGroup secondaryDictionaryGroup;
|
||||||
|
if (secondaryLocale == null)
|
||||||
|
secondaryDictionaryGroup = null;
|
||||||
|
else
|
||||||
|
secondaryDictionaryGroup = findDictionaryGroupWithLocale(mSecondaryDictionaryGroup, secondaryLocale);
|
||||||
|
final Dictionary secondaryMainDict;
|
||||||
|
if (secondaryLocale == null)
|
||||||
|
secondaryMainDict = null;
|
||||||
|
else
|
||||||
|
secondaryMainDict = DictionaryFactory.createMainDictionaryFromManager(context, secondaryLocale);
|
||||||
|
|
||||||
synchronized (mLock) {
|
synchronized (mLock) {
|
||||||
if (locale.equals(dictionaryGroup.mLocale)) {
|
if (locale.equals(dictionaryGroup.mLocale)) {
|
||||||
dictionaryGroup.setMainDict(mainDict);
|
dictionaryGroup.setMainDict(mainDict);
|
||||||
|
@ -414,6 +524,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
// Dictionary facilitator has been reset for another locale.
|
// Dictionary facilitator has been reset for another locale.
|
||||||
mainDict.close();
|
mainDict.close();
|
||||||
}
|
}
|
||||||
|
if (secondaryDictionaryGroup != null && secondaryLocale.equals(secondaryDictionaryGroup.mLocale))
|
||||||
|
secondaryDictionaryGroup.setMainDict(secondaryMainDict);
|
||||||
|
else if (secondaryMainDict != null)
|
||||||
|
secondaryMainDict.close();
|
||||||
}
|
}
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary());
|
listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary());
|
||||||
|
@ -453,13 +567,19 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void closeDictionaries() {
|
public void closeDictionaries() {
|
||||||
final DictionaryGroup dictionaryGroupToClose;
|
final DictionaryGroup mainDictionaryGroupToClose;
|
||||||
|
final DictionaryGroup secondaryDictionaryGroupToClose;
|
||||||
synchronized (mLock) {
|
synchronized (mLock) {
|
||||||
dictionaryGroupToClose = mDictionaryGroup;
|
mainDictionaryGroupToClose = mDictionaryGroup;
|
||||||
|
secondaryDictionaryGroupToClose = mSecondaryDictionaryGroup;
|
||||||
mDictionaryGroup = new DictionaryGroup();
|
mDictionaryGroup = new DictionaryGroup();
|
||||||
|
if (mSecondaryDictionaryGroup != null)
|
||||||
|
mSecondaryDictionaryGroup = new DictionaryGroup();
|
||||||
}
|
}
|
||||||
for (final String dictType : ALL_DICTIONARY_TYPES) {
|
for (final String dictType : ALL_DICTIONARY_TYPES) {
|
||||||
dictionaryGroupToClose.closeDict(dictType);
|
mainDictionaryGroupToClose.closeDict(dictType);
|
||||||
|
if (secondaryDictionaryGroupToClose != null)
|
||||||
|
secondaryDictionaryGroupToClose.closeDict(dictType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,6 +597,11 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
|
|
||||||
public boolean hasAtLeastOneUninitializedMainDictionary() {
|
public boolean hasAtLeastOneUninitializedMainDictionary() {
|
||||||
final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN);
|
final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN);
|
||||||
|
if (mSecondaryDictionaryGroup != null) {
|
||||||
|
final Dictionary secondaryDict = mSecondaryDictionaryGroup.getDict(Dictionary.TYPE_MAIN);
|
||||||
|
if (secondaryDict == null || !secondaryDict.isInitialized())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return mainDict == null || !mainDict.isInitialized();
|
return mainDict == null || !mainDict.isInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -502,11 +627,30 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
putWordIntoValidSpellingWordCache("addToUserHistory", suggestion);
|
putWordIntoValidSpellingWordCache("addToUserHistory", suggestion);
|
||||||
|
|
||||||
final String[] words = suggestion.split(Constants.WORD_SEPARATOR);
|
final String[] words = suggestion.split(Constants.WORD_SEPARATOR);
|
||||||
|
|
||||||
|
// increase / decrease confidence if we have a secondary dictionary group
|
||||||
|
if (mSecondaryDictionaryGroup != null && words.length == 1) {
|
||||||
|
// if suggestion was auto-capitalized, check against both the suggestion and the de-capitalized suggestion
|
||||||
|
final String decapitalizedSuggestion;
|
||||||
|
if (wasAutoCapitalized)
|
||||||
|
decapitalizedSuggestion = suggestion.substring(0, 1).toLowerCase() + suggestion.substring(1);
|
||||||
|
else
|
||||||
|
decapitalizedSuggestion = suggestion;
|
||||||
|
if ((wasAutoCapitalized && isValidWord(decapitalizedSuggestion, ALL_DICTIONARY_TYPES, mDictionaryGroup))
|
||||||
|
|| isValidWord(suggestion, ALL_DICTIONARY_TYPES, mDictionaryGroup))
|
||||||
|
mDictionaryGroup.increaseConfidence();
|
||||||
|
else mDictionaryGroup.decreaseConfidence();
|
||||||
|
if ((wasAutoCapitalized && isValidWord(decapitalizedSuggestion, ALL_DICTIONARY_TYPES, mSecondaryDictionaryGroup))
|
||||||
|
|| isValidWord(suggestion, ALL_DICTIONARY_TYPES, mSecondaryDictionaryGroup))
|
||||||
|
mSecondaryDictionaryGroup.increaseConfidence();
|
||||||
|
else mSecondaryDictionaryGroup.decreaseConfidence();
|
||||||
|
}
|
||||||
NgramContext ngramContextForCurrentWord = ngramContext;
|
NgramContext ngramContextForCurrentWord = ngramContext;
|
||||||
for (int i = 0; i < words.length; i++) {
|
for (int i = 0; i < words.length; i++) {
|
||||||
final String currentWord = words[i];
|
final String currentWord = words[i];
|
||||||
final boolean wasCurrentWordAutoCapitalized = (i == 0) && wasAutoCapitalized;
|
final boolean wasCurrentWordAutoCapitalized = (i == 0) && wasAutoCapitalized;
|
||||||
addWordToUserHistory(mDictionaryGroup, ngramContextForCurrentWord, currentWord,
|
// add to history for preferred dictionary group, to avoid mixing languages in history
|
||||||
|
addWordToUserHistory(getCurrentlyPreferredDictionaryGroup(), ngramContextForCurrentWord, currentWord,
|
||||||
wasCurrentWordAutoCapitalized, (int) timeStampInSeconds,
|
wasCurrentWordAutoCapitalized, (int) timeStampInSeconds,
|
||||||
blockPotentiallyOffensive);
|
blockPotentiallyOffensive);
|
||||||
ngramContextForCurrentWord =
|
ngramContextForCurrentWord =
|
||||||
|
@ -542,17 +686,19 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
final int timeStampInSeconds, final boolean blockPotentiallyOffensive) {
|
final int timeStampInSeconds, final boolean blockPotentiallyOffensive) {
|
||||||
final ExpandableBinaryDictionary userHistoryDictionary =
|
final ExpandableBinaryDictionary userHistoryDictionary =
|
||||||
dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY);
|
dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY);
|
||||||
if (userHistoryDictionary == null || !isForLocale(userHistoryDictionary.mLocale)) {
|
if (userHistoryDictionary == null || !hasLocale(userHistoryDictionary.mLocale)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final int maxFreq = getFrequency(word);
|
final int maxFreq = getFrequency(word, dictionaryGroup);
|
||||||
if (maxFreq == 0 && blockPotentiallyOffensive) {
|
if (maxFreq == 0 && blockPotentiallyOffensive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale);
|
|
||||||
final String secondWord;
|
final String secondWord;
|
||||||
if (wasAutoCapitalized) {
|
if (wasAutoCapitalized) {
|
||||||
if (isValidSuggestionWord(word) && !isValidSuggestionWord(lowerCasedWord)) {
|
// used word with lower-case first letter instead of all lower-case, as auto-capitalize
|
||||||
|
// does not affect the other letters
|
||||||
|
final String decapitalizedWord = word.substring(0, 1).toLowerCase(dictionaryGroup.mLocale) + word.substring(1);
|
||||||
|
if (isValidWord(word, ALL_DICTIONARY_TYPES, dictionaryGroup) && !isValidWord(decapitalizedWord, ALL_DICTIONARY_TYPES, dictionaryGroup)) {
|
||||||
// If the word was auto-capitalized and exists only as a capitalized word in the
|
// 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,
|
// 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
|
// the name of the contacts in start-of-sentence position would come here with the
|
||||||
|
@ -560,15 +706,16 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
// of that contact's name which would end up popping in suggestions.
|
// of that contact's name which would end up popping in suggestions.
|
||||||
secondWord = word;
|
secondWord = word;
|
||||||
} else {
|
} else {
|
||||||
// If however the word is not in the dictionary, or exists as a lower-case word
|
// If however the word is not in the dictionary, or exists as a de-capitalized word
|
||||||
// only, then we consider that was a lower-case word that had been auto-capitalized.
|
// only, then we consider that was a lower-case word that had been auto-capitalized.
|
||||||
secondWord = lowerCasedWord;
|
secondWord = decapitalizedWord;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// HACK: We'd like to avoid adding the capitalized form of common words to the User
|
// 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
|
// History dictionary in order to avoid suggesting them until the dictionary
|
||||||
// consolidation is done.
|
// consolidation is done.
|
||||||
// TODO: Remove this hack when ready.
|
// TODO: Remove this hack when ready.
|
||||||
|
final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale);
|
||||||
final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN,
|
final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN,
|
||||||
null /* account */) ?
|
null /* account */) ?
|
||||||
dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) :
|
dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) :
|
||||||
|
@ -588,8 +735,18 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
isValid, timeStampInSeconds);
|
isValid, timeStampInSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** returns the dictionaryGroup with most confidence, main group when tied */
|
||||||
|
private DictionaryGroup getCurrentlyPreferredDictionaryGroup() {
|
||||||
|
final DictionaryGroup dictGroup;
|
||||||
|
if (mSecondaryDictionaryGroup == null || mSecondaryDictionaryGroup.mConfidence <= mDictionaryGroup.mConfidence)
|
||||||
|
dictGroup = mDictionaryGroup;
|
||||||
|
else
|
||||||
|
dictGroup = mSecondaryDictionaryGroup;
|
||||||
|
return dictGroup;
|
||||||
|
}
|
||||||
|
|
||||||
private void removeWord(final String dictName, final String word) {
|
private void removeWord(final String dictName, final String word) {
|
||||||
final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName);
|
final ExpandableBinaryDictionary dictionary = getCurrentlyPreferredDictionaryGroup().getSubDict(dictName);
|
||||||
if (dictionary != null) {
|
if (dictionary != null) {
|
||||||
dictionary.removeUnigramEntryDynamically(word);
|
dictionary.removeUnigramEntryDynamically(word);
|
||||||
}
|
}
|
||||||
|
@ -621,25 +778,70 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
false /* firstSuggestionExceedsConfidenceThreshold */);
|
false /* firstSuggestionExceedsConfidenceThreshold */);
|
||||||
final float[] weightOfLangModelVsSpatialModel =
|
final float[] weightOfLangModelVsSpatialModel =
|
||||||
new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL };
|
new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL };
|
||||||
|
|
||||||
|
// start getting suggestions for secondary locale first, but in separate thread
|
||||||
|
final ArrayList<SuggestedWordInfo> dictionarySuggestionsSecondary = new ArrayList<>();
|
||||||
|
final CountDownLatch waitForSecondaryDictionary = new CountDownLatch(1);
|
||||||
|
if (mSecondaryDictionaryGroup != null) {
|
||||||
|
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
dictionarySuggestionsSecondary.addAll(getSuggestions(composedData,
|
||||||
|
ngramContext, settingsValuesForSuggestion, sessionId, proximityInfoHandle,
|
||||||
|
weightOfLangModelVsSpatialModel, mSecondaryDictionaryGroup));
|
||||||
|
waitForSecondaryDictionary.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// get main locale suggestions
|
||||||
|
final ArrayList<SuggestedWordInfo> dictionarySuggestions = getSuggestions(composedData,
|
||||||
|
ngramContext, settingsValuesForSuggestion, sessionId, proximityInfoHandle,
|
||||||
|
weightOfLangModelVsSpatialModel, mDictionaryGroup);
|
||||||
|
suggestionResults.addAll(dictionarySuggestions);
|
||||||
|
if (null != suggestionResults.mRawSuggestions) {
|
||||||
|
suggestionResults.mRawSuggestions.addAll(dictionarySuggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for secondary locale suggestions
|
||||||
|
if (mSecondaryDictionaryGroup != null) {
|
||||||
|
try { waitForSecondaryDictionary.await(); }
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
Log.w(TAG, "Interrupted while trying to get secondary locale suggestions", e);
|
||||||
|
}
|
||||||
|
suggestionResults.addAll(dictionarySuggestionsSecondary);
|
||||||
|
if (null != suggestionResults.mRawSuggestions) {
|
||||||
|
suggestionResults.mRawSuggestions.addAll(dictionarySuggestionsSecondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestionResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<SuggestedWordInfo> getSuggestions(ComposedData composedData,
|
||||||
|
NgramContext ngramContext, SettingsValuesForSuggestion settingsValuesForSuggestion,
|
||||||
|
int sessionId, long proximityInfoHandle, float[] weightOfLangModelVsSpatialModel,
|
||||||
|
DictionaryGroup dictGroup) {
|
||||||
|
final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>();
|
||||||
for (final String dictType : ALL_DICTIONARY_TYPES) {
|
for (final String dictType : ALL_DICTIONARY_TYPES) {
|
||||||
final Dictionary dictionary = mDictionaryGroup.getDict(dictType);
|
final Dictionary dictionary = dictGroup.getDict(dictType);
|
||||||
if (null == dictionary) continue;
|
if (null == dictionary) continue;
|
||||||
final float weightForLocale = composedData.mIsBatchMode
|
final float weightForLocale = composedData.mIsBatchMode
|
||||||
? mDictionaryGroup.mWeightForGesturingInLocale
|
? dictGroup.mWeightForGesturingInLocale
|
||||||
: mDictionaryGroup.mWeightForTypingInLocale;
|
: dictGroup.mWeightForTypingInLocale;
|
||||||
final ArrayList<SuggestedWordInfo> dictionarySuggestions =
|
final ArrayList<SuggestedWordInfo> dictionarySuggestions =
|
||||||
dictionary.getSuggestions(composedData, ngramContext,
|
dictionary.getSuggestions(composedData, ngramContext,
|
||||||
proximityInfoHandle, settingsValuesForSuggestion, sessionId,
|
proximityInfoHandle, settingsValuesForSuggestion, sessionId,
|
||||||
weightForLocale, weightOfLangModelVsSpatialModel);
|
weightForLocale, weightOfLangModelVsSpatialModel);
|
||||||
if (null == dictionarySuggestions) continue;
|
if (null == dictionarySuggestions) continue;
|
||||||
suggestionResults.addAll(dictionarySuggestions);
|
suggestions.addAll(dictionarySuggestions);
|
||||||
if (null != suggestionResults.mRawSuggestions) {
|
|
||||||
suggestionResults.mRawSuggestions.addAll(dictionarySuggestions);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return suggestionResults;
|
return suggestions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spell checker is using this, and has its own instance of DictionaryFacilitatorImpl,
|
||||||
|
// meaning that it always has default mConfidence. So we cannot choose to only check preferred
|
||||||
|
// locale, and instead simply return true if word is in any of the available dictionaries
|
||||||
public boolean isValidSpellingWord(final String word) {
|
public boolean isValidSpellingWord(final String word) {
|
||||||
if (mValidSpellingWordReadCache != null) {
|
if (mValidSpellingWordReadCache != null) {
|
||||||
final Boolean cachedValue = mValidSpellingWordReadCache.get(word);
|
final Boolean cachedValue = mValidSpellingWordReadCache.get(word);
|
||||||
|
@ -648,22 +850,23 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return isValidWord(word, ALL_DICTIONARY_TYPES);
|
return isValidWord(word, ALL_DICTIONARY_TYPES, mDictionaryGroup) ||
|
||||||
|
(mSecondaryDictionaryGroup != null && isValidWord(word, ALL_DICTIONARY_TYPES, mSecondaryDictionaryGroup));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValidSuggestionWord(final String word) {
|
public boolean isValidSuggestionWord(final String word) {
|
||||||
return isValidWord(word, ALL_DICTIONARY_TYPES);
|
return isValidWord(word, ALL_DICTIONARY_TYPES, mDictionaryGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidWord(final String word, final String[] dictionariesToCheck) {
|
private boolean isValidWord(final String word, final String[] dictionariesToCheck, final DictionaryGroup dictionaryGroup) {
|
||||||
if (TextUtils.isEmpty(word)) {
|
if (TextUtils.isEmpty(word)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (mDictionaryGroup.mLocale == null) {
|
if (dictionaryGroup.mLocale == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (final String dictType : dictionariesToCheck) {
|
for (final String dictType : dictionariesToCheck) {
|
||||||
final Dictionary dictionary = mDictionaryGroup.getDict(dictType);
|
final Dictionary dictionary = dictionaryGroup.getDict(dictType);
|
||||||
// Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
|
// 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
|
// would be immutable once it's finished initializing, but concretely a null test is
|
||||||
// probably good enough for the time being.
|
// probably good enough for the time being.
|
||||||
|
@ -675,13 +878,18 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getFrequency(final String word) {
|
// called from addWordToUserHistory with a specified dictionary, so provide this dictionary
|
||||||
|
private int getFrequency(final String word, DictionaryGroup dictGroup) {
|
||||||
if (TextUtils.isEmpty(word)) {
|
if (TextUtils.isEmpty(word)) {
|
||||||
return Dictionary.NOT_A_PROBABILITY;
|
return Dictionary.NOT_A_PROBABILITY;
|
||||||
}
|
}
|
||||||
int maxFreq = Dictionary.NOT_A_PROBABILITY;
|
int maxFreq = Dictionary.NOT_A_PROBABILITY;
|
||||||
|
// ExpandableBinaryDictionary (means: all except main) always return NOT_A_PROBABILITY
|
||||||
|
// because it doesn't override getFrequency()
|
||||||
|
// So why is it checked anyway?
|
||||||
|
// Is this a bug, or intended by AOSP devs?
|
||||||
for (final String dictType : ALL_DICTIONARY_TYPES) {
|
for (final String dictType : ALL_DICTIONARY_TYPES) {
|
||||||
final Dictionary dictionary = mDictionaryGroup.getDict(dictType);
|
final Dictionary dictionary = dictGroup.getDict(dictType);
|
||||||
if (dictionary == null) continue;
|
if (dictionary == null) continue;
|
||||||
final int tempFreq = dictionary.getFrequency(word);
|
final int tempFreq = dictionary.getFrequency(word);
|
||||||
if (tempFreq >= maxFreq) {
|
if (tempFreq >= maxFreq) {
|
||||||
|
@ -697,6 +905,12 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
dictionary.clear();
|
dictionary.clear();
|
||||||
|
// called when not using personalized dictionaries, so should also reset secondary user history
|
||||||
|
if (mSecondaryDictionaryGroup != null) {
|
||||||
|
final ExpandableBinaryDictionary secondaryDictionary = mSecondaryDictionaryGroup.getSubDict(dictName);
|
||||||
|
if (secondaryDictionary != null)
|
||||||
|
secondaryDictionary.clear();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 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 org.dslul.openboard.inputmethod.latin.settings;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.view.inputmethod.InputMethodSubtype;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.BuildConfig;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.RichInputMethodManager;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.DialogUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class SecondaryLocaleSettingsFragment extends SubScreenFragment {
|
||||||
|
private RichInputMethodManager mRichImm;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
RichInputMethodManager.init(getActivity());
|
||||||
|
mRichImm = RichInputMethodManager.getInstance();
|
||||||
|
addPreferencesFromResource(R.xml.additional_subtype_settings);
|
||||||
|
resetKeyboardLocales();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetKeyboardLocales() {
|
||||||
|
mRichImm.refreshSubtypeCaches();
|
||||||
|
getPreferenceScreen().removeAll();
|
||||||
|
final Context context = getActivity();
|
||||||
|
List<InputMethodSubtype> subtypes = mRichImm.getMyEnabledInputMethodSubtypeList(true);
|
||||||
|
|
||||||
|
for (InputMethodSubtype subtype : subtypes) {
|
||||||
|
final Locale secondaryLocale = Settings.getSecondaryLocale(getSharedPreferences(), subtype.getLocale());
|
||||||
|
final Preference pref = new Preference(context);
|
||||||
|
pref.setTitle(subtype.getDisplayName(context, BuildConfig.APPLICATION_ID, context.getApplicationInfo()));
|
||||||
|
if (secondaryLocale != null)
|
||||||
|
pref.setSummary(secondaryLocale.getDisplayLanguage(getResources().getConfiguration().locale));
|
||||||
|
|
||||||
|
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
|
showSecondaryLocaleDialog(subtype.getLocale().toLowerCase(Locale.ENGLISH), subtype.isAsciiCapable());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
getPreferenceScreen().addPreference(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSecondaryLocaleDialog(String mainLocale, boolean asciiCapable) {
|
||||||
|
final List<String> locales = new ArrayList<>(getAvailableDictionaryLocales(mainLocale, asciiCapable));
|
||||||
|
Collections.sort(locales);
|
||||||
|
|
||||||
|
// we don't want to offer mainLocale as a choice, same goes for the language (e.g. en for en_GB)
|
||||||
|
locales.remove(mainLocale);
|
||||||
|
if (mainLocale.contains("_")) {
|
||||||
|
final String mainLanguage = LocaleUtils.constructLocaleFromString(mainLocale).getLanguage();
|
||||||
|
locales.remove(mainLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
final AlertDialog.Builder builder = new AlertDialog.Builder(
|
||||||
|
DialogUtils.getPlatformDialogThemeContext(getActivity()))
|
||||||
|
.setTitle(R.string.language_selection_title)
|
||||||
|
.setPositiveButton(android.R.string.cancel, null);
|
||||||
|
|
||||||
|
if (locales.isEmpty()) {
|
||||||
|
builder.setMessage(R.string.no_secondary_locales)
|
||||||
|
.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert "no secondary language" option on top
|
||||||
|
locales.add(0, getResources().getString(R.string.secondary_locale_none));
|
||||||
|
|
||||||
|
final Locale displayLocale = getResources().getConfiguration().locale;
|
||||||
|
final CharSequence[] titles = locales.toArray(new CharSequence[0]);
|
||||||
|
for (int i = 1; i < titles.length ; i++) {
|
||||||
|
final Locale loc = LocaleUtils.constructLocaleFromString(titles[i].toString());
|
||||||
|
titles[i] = loc.getDisplayName(displayLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
Locale currentSecondaryLocale = Settings.getSecondaryLocale(getSharedPreferences(), mainLocale);
|
||||||
|
int checkedItem;
|
||||||
|
if (currentSecondaryLocale == null)
|
||||||
|
checkedItem = 0;
|
||||||
|
else
|
||||||
|
checkedItem = locales.indexOf(currentSecondaryLocale.toString());
|
||||||
|
|
||||||
|
builder.setSingleChoiceItems(titles, checkedItem, (dialogInterface, i) -> {
|
||||||
|
String locale = locales.get(i);
|
||||||
|
if (i == 0)
|
||||||
|
locale = "";
|
||||||
|
final Set<String> encodedLocales = new HashSet<>();
|
||||||
|
boolean updated = false;
|
||||||
|
for (String encodedLocale : getSharedPreferences().getStringSet(Settings.PREF_SECONDARY_LOCALES, new HashSet<>())) {
|
||||||
|
String[] locs = encodedLocale.split("§");
|
||||||
|
if (locs.length == 2 && locs[0].equals(mainLocale)) {
|
||||||
|
if (!locale.isEmpty())
|
||||||
|
encodedLocales.add(mainLocale + "§" + locale);
|
||||||
|
updated = true;
|
||||||
|
} else {
|
||||||
|
encodedLocales.add(encodedLocale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!updated)
|
||||||
|
encodedLocales.add(mainLocale + "§" + locale);
|
||||||
|
getSharedPreferences().edit().putStringSet(Settings.PREF_SECONDARY_LOCALES, encodedLocales).apply();
|
||||||
|
final Intent newDictBroadcast = new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
|
||||||
|
getActivity().sendBroadcast(newDictBroadcast);
|
||||||
|
resetKeyboardLocales();
|
||||||
|
dialogInterface.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// get locales with same script as main locale, but different language
|
||||||
|
private Set<String> getAvailableDictionaryLocales(String mainLocale, boolean asciiCapable) {
|
||||||
|
final Locale mainL = LocaleUtils.constructLocaleFromString(mainLocale);
|
||||||
|
final Set<String> locales = new HashSet<>();
|
||||||
|
final int mainScript;
|
||||||
|
if (asciiCapable)
|
||||||
|
mainScript = ScriptUtils.SCRIPT_LATIN;
|
||||||
|
else
|
||||||
|
mainScript = ScriptUtils.getScriptFromSpellCheckerLocale(mainL);
|
||||||
|
// ScriptUtils.getScriptFromSpellCheckerLocale may return latin when it should not
|
||||||
|
// e.g. for persian or chinese
|
||||||
|
// workaround: don't allow secondary locales for these locales
|
||||||
|
if (!asciiCapable && mainScript == ScriptUtils.SCRIPT_LATIN)
|
||||||
|
return locales;
|
||||||
|
|
||||||
|
// get cached dictionaries: extracted or user-added dictionaries
|
||||||
|
final File[] cachedDirectoryList = DictionaryInfoUtils.getCachedDirectoryList(getActivity());
|
||||||
|
if (cachedDirectoryList != null) {
|
||||||
|
for (File directory : cachedDirectoryList) {
|
||||||
|
if (!directory.isDirectory()) continue;
|
||||||
|
final String dirLocale =
|
||||||
|
DictionaryInfoUtils.getWordListIdFromFileName(directory.getName());
|
||||||
|
if (dirLocale.equals(mainLocale)) continue;
|
||||||
|
final Locale locale = LocaleUtils.constructLocaleFromString(dirLocale);
|
||||||
|
if (locale.getLanguage().equals(mainL.getLanguage())) continue;
|
||||||
|
int localeScript = ScriptUtils.getScriptFromSpellCheckerLocale(locale);
|
||||||
|
if (localeScript != mainScript) continue;
|
||||||
|
locales.add(locale.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// get assets dictionaries
|
||||||
|
final String[] assetsDictionaryList = BinaryDictionaryGetter.getAssetsDictionaryList(getActivity());
|
||||||
|
if (assetsDictionaryList != null) {
|
||||||
|
for (String dictionary : assetsDictionaryList) {
|
||||||
|
final String dictLocale =
|
||||||
|
BinaryDictionaryGetter.extractLocaleFromAssetsDictionaryFile(dictionary);
|
||||||
|
if (dictLocale == null) continue;
|
||||||
|
if (dictLocale.equals(mainLocale)) continue;
|
||||||
|
final Locale locale = LocaleUtils.constructLocaleFromString(dictLocale);
|
||||||
|
if (locale.getLanguage().equals(mainL.getLanguage())) continue;
|
||||||
|
int localeScript = ScriptUtils.getScriptFromSpellCheckerLocale(locale);
|
||||||
|
if (localeScript != mainScript) continue;
|
||||||
|
locales.add(locale.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return locales;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -29,15 +29,18 @@ import android.view.Gravity;
|
||||||
import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager;
|
import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager;
|
||||||
import org.dslul.openboard.inputmethod.latin.InputAttributes;
|
import org.dslul.openboard.inputmethod.latin.InputAttributes;
|
||||||
import org.dslul.openboard.inputmethod.latin.R;
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.AdditionalSubtypeUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.AdditionalSubtypeUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.JniUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.JniUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.RunInLocale;
|
import org.dslul.openboard.inputmethod.latin.utils.RunInLocale;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.StatsUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.StatsUtils;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
@ -126,6 +129,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
||||||
public static final String PREF_ENABLE_CLIPBOARD_HISTORY = "pref_enable_clipboard_history";
|
public static final String PREF_ENABLE_CLIPBOARD_HISTORY = "pref_enable_clipboard_history";
|
||||||
public static final String PREF_CLIPBOARD_HISTORY_RETENTION_TIME = "pref_clipboard_history_retention_time";
|
public static final String PREF_CLIPBOARD_HISTORY_RETENTION_TIME = "pref_clipboard_history_retention_time";
|
||||||
|
|
||||||
|
public static final String PREF_SECONDARY_LOCALES = "pref_secondary_locales";
|
||||||
|
|
||||||
// This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead.
|
// This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead.
|
||||||
// This is being used only for the backward compatibility.
|
// This is being used only for the backward compatibility.
|
||||||
private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
|
private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
|
||||||
|
@ -517,4 +522,15 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
|
||||||
final SharedPreferences prefs, final int defValue) {
|
final SharedPreferences prefs, final int defValue) {
|
||||||
return prefs.getInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID, defValue);
|
return prefs.getInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID, defValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Locale getSecondaryLocale(final SharedPreferences prefs, final String mainLocaleString) {
|
||||||
|
final Set<String> encodedLocales = prefs.getStringSet(PREF_SECONDARY_LOCALES, new HashSet<>());
|
||||||
|
for (String loc : encodedLocales) {
|
||||||
|
String[] locales = loc.split("§");
|
||||||
|
if (locales.length == 2 && locales[0].equals(mainLocaleString.toLowerCase(Locale.ENGLISH)))
|
||||||
|
return LocaleUtils.constructLocaleFromString(locales[1]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@ import org.dslul.openboard.inputmethod.compat.AppWorkaroundsUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.InputAttributes;
|
import org.dslul.openboard.inputmethod.latin.InputAttributes;
|
||||||
import org.dslul.openboard.inputmethod.latin.R;
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
import org.dslul.openboard.inputmethod.latin.RichInputMethodManager;
|
import org.dslul.openboard.inputmethod.latin.RichInputMethodManager;
|
||||||
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.AsyncResultHolder;
|
import org.dslul.openboard.inputmethod.latin.utils.AsyncResultHolder;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
|
||||||
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils;
|
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils;
|
||||||
|
@ -86,6 +85,7 @@ public class SettingsValues {
|
||||||
public final long mClipboardHistoryRetentionTime;
|
public final long mClipboardHistoryRetentionTime;
|
||||||
public final boolean mOneHandedModeEnabled;
|
public final boolean mOneHandedModeEnabled;
|
||||||
public final int mOneHandedModeGravity;
|
public final int mOneHandedModeGravity;
|
||||||
|
public final Locale mSecondaryLocale;
|
||||||
// Use bigrams to predict the next word when there is no input for it yet
|
// Use bigrams to predict the next word when there is no input for it yet
|
||||||
public final boolean mBigramPredictionEnabled;
|
public final boolean mBigramPredictionEnabled;
|
||||||
public final boolean mGestureInputEnabled;
|
public final boolean mGestureInputEnabled;
|
||||||
|
@ -242,6 +242,7 @@ public class SettingsValues {
|
||||||
mClipboardHistoryRetentionTime = Settings.readClipboardHistoryRetentionTime(prefs, res);
|
mClipboardHistoryRetentionTime = Settings.readClipboardHistoryRetentionTime(prefs, res);
|
||||||
mOneHandedModeEnabled = Settings.readOneHandedModeEnabled(prefs);
|
mOneHandedModeEnabled = Settings.readOneHandedModeEnabled(prefs);
|
||||||
mOneHandedModeGravity = Settings.readOneHandedModeGravity(prefs);
|
mOneHandedModeGravity = Settings.readOneHandedModeGravity(prefs);
|
||||||
|
mSecondaryLocale = Settings.getSecondaryLocale(prefs, RichInputMethodManager.getInstance().getCurrentSubtypeLocale().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isMetricsLoggingEnabled() {
|
public boolean isMetricsLoggingEnabled() {
|
||||||
|
|
|
@ -201,6 +201,9 @@ public class ScriptUtils {
|
||||||
* {@see http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes}
|
* {@see http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes}
|
||||||
*/
|
*/
|
||||||
public static int getScriptFromSpellCheckerLocale(final Locale locale) {
|
public static int getScriptFromSpellCheckerLocale(final Locale locale) {
|
||||||
|
// need special treatment of serbian latin, which would get detected as cyrillic
|
||||||
|
if (locale.toString().toLowerCase(Locale.ENGLISH).equals("sr_zz"))
|
||||||
|
return ScriptUtils.SCRIPT_LATIN;
|
||||||
String language = locale.getLanguage();
|
String language = locale.getLanguage();
|
||||||
Integer script = mLanguageCodeToScriptCode.get(language);
|
Integer script = mLanguageCodeToScriptCode.get(language);
|
||||||
if (script == null) {
|
if (script == null) {
|
||||||
|
|
|
@ -186,6 +186,14 @@
|
||||||
<string name="delete_swipe_summary">Perform a swipe from the delete key to select and remove bigger portions of text at once</string>
|
<string name="delete_swipe_summary">Perform a swipe from the delete key to select and remove bigger portions of text at once</string>
|
||||||
<!-- Preferences item for enabling trackpad space key -->
|
<!-- Preferences item for enabling trackpad space key -->
|
||||||
<string name="space_trackpad">Space bar trackpad</string>
|
<string name="space_trackpad">Space bar trackpad</string>
|
||||||
|
<!-- Preferences item for choosing secondary language -->
|
||||||
|
<string name="secondary_locale">Multilingual typing</string>
|
||||||
|
<!-- Description for "secondary_locale" option. -->
|
||||||
|
<string name="secondary_locale_summary">Select a secondary dictionary to use alongside main language</string>
|
||||||
|
<!-- Text when no secondary locale chosen -->
|
||||||
|
<string name="secondary_locale_none">None</string>
|
||||||
|
<!-- Message shown when no secondary locales available -->
|
||||||
|
<string name="no_secondary_locales">No secondary dictionaries available</string>
|
||||||
<!-- Description for "space_trackpad" option. -->
|
<!-- Description for "space_trackpad" option. -->
|
||||||
<string name="space_trackpad_summary">Swipe on the spacebar to move the cursor</string>
|
<string name="space_trackpad_summary">Swipe on the spacebar to move the cursor</string>
|
||||||
<!-- Preferences item for enabling inserting more spaces key -->
|
<!-- Preferences item for enabling inserting more spaces key -->
|
||||||
|
|
|
@ -82,6 +82,12 @@
|
||||||
android:summary="@string/autospace_after_punctuation_summary"
|
android:summary="@string/autospace_after_punctuation_summary"
|
||||||
android:defaultValue="false" />
|
android:defaultValue="false" />
|
||||||
|
|
||||||
|
<PreferenceScreen
|
||||||
|
android:fragment="org.dslul.openboard.inputmethod.latin.settings.SecondaryLocaleSettingsFragment"
|
||||||
|
android:key="pref_secondary_locales"
|
||||||
|
android:title="@string/secondary_locale"
|
||||||
|
android:summary="@string/secondary_locale_summary" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue