add multilingual typing

This commit is contained in:
Helium314 2023-06-26 23:47:21 +02:00
parent 0f585f9f1b
commit ac7ac4f907
9 changed files with 504 additions and 35 deletions

View file

@ -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
* ~user-selectable dictionaries, https://github.com/openboard-team/openboard/pull/578~
* ~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~
* ~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
@ -19,6 +21,7 @@ Plan / to do:
* delete suggestions, https://github.com/openboard-team/openboard/issues/106
* glide typing, https://github.com/openboard-team/openboard/issues/3
* license issues, maybe allow using an external library
* re-consider preferring lowercase word for typed history in some cases (DictionaryFacilitatorImpl.addWordToUserHistory)
Changes:
* Updated dependencies
@ -34,6 +37,7 @@ Changes:
* 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
* 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
-----

View file

@ -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.CoordinateUtils;
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.LanguageOnSpacebarUtils;
import org.dslul.openboard.inputmethod.latin.utils.TypefaceUtils;
import java.util.Locale;
import java.util.WeakHashMap;
import javax.annotation.Nonnull;
@ -838,6 +840,25 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
private String layoutLanguageOnSpacebar(final Paint paint,
final RichInputMethodSubtype subtype, final int 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) {
final String fullText = subtype.getFullDisplayName();
if (fitsTextIntoWidth(width, fullText, paint)) {

View file

@ -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.permissions.PermissionsUtil;
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.utils.ExecutorUtils;
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils;
import org.dslul.openboard.inputmethod.latin.utils.SuggestionResults;
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 DictionaryGroup mDictionaryGroup = new DictionaryGroup();
private DictionaryGroup mSecondaryDictionaryGroup = new DictionaryGroup();
private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0);
// To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
private final Object mLock = new Object();
@ -86,6 +89,9 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES =
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> mValidSpellingWordWriteCache;
@ -104,6 +110,11 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
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.
*
@ -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_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.
*/
@ -137,8 +151,34 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
@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;
// has been committed in a row, with an exception when typing a single word not contained
// 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 mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
@ -229,6 +269,11 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
Dictionary dict = mDictionaryGroup.getDict(dictType);
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
@ -276,10 +321,15 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
@Nullable
static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup dictionaryGroup,
final Locale locale) {
if (dictionaryGroup == null) return null;
return locale.equals(dictionaryGroup.mLocale) ? dictionaryGroup : null;
}
@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(
final Context context,
final Locale newLocale,
@ -353,13 +403,56 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
DictionaryGroup newDictionaryGroup =
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.
final DictionaryGroup oldDictionaryGroup;
final DictionaryGroup oldSecondaryDictionaryGroup;
synchronized (mLock) {
oldDictionaryGroup = mDictionaryGroup;
mDictionaryGroup = newDictionaryGroup;
oldSecondaryDictionaryGroup = mSecondaryDictionaryGroup;
mSecondaryDictionaryGroup = newSecondaryDictionaryGroup;
if (hasAtLeastOneUninitializedMainDictionary()) {
asyncReloadUninitializedMainDictionaries(context, newLocale, listener);
asyncReloadUninitializedMainDictionaries(context, newLocale,
mSecondaryDictionaryGroup == null ? null : secondaryLocale, listener);
}
}
if (listener != null) {
@ -370,8 +463,13 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
for (final Locale localeToCleanUp : existingDictionariesToCleanup.keySet()) {
final ArrayList<String> dictTypesToCleanUp =
existingDictionariesToCleanup.get(localeToCleanUp);
final DictionaryGroup dictionarySetToCleanup =
DictionaryGroup dictionarySetToCleanup =
findDictionaryGroupWithLocale(oldDictionaryGroup, localeToCleanUp);
if (dictionarySetToCleanup == null)
dictionarySetToCleanup =
findDictionaryGroupWithLocale(oldSecondaryDictionaryGroup, localeToCleanUp);
if (dictionarySetToCleanup == null)
continue;
for (final String dictType : dictTypesToCleanUp) {
dictionarySetToCleanup.closeDict(dictType);
}
@ -383,20 +481,20 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
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);
mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary;
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
@Override
public void run() {
doReloadUninitializedMainDictionaries(
context, locale, listener, latchForWaitingLoadingMainDictionary);
context, locale, secondaryLocale, listener, latchForWaitingLoadingMainDictionary);
}
});
}
void doReloadUninitializedMainDictionaries(final Context context, final Locale locale,
final DictionaryInitializationListener listener,
final Locale secondaryLocale, final DictionaryInitializationListener listener,
final CountDownLatch latchForWaitingLoadingMainDictionary) {
final DictionaryGroup dictionaryGroup =
findDictionaryGroupWithLocale(mDictionaryGroup, locale);
@ -407,6 +505,18 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
final Dictionary mainDict =
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) {
if (locale.equals(dictionaryGroup.mLocale)) {
dictionaryGroup.setMainDict(mainDict);
@ -414,6 +524,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
// Dictionary facilitator has been reset for another locale.
mainDict.close();
}
if (secondaryDictionaryGroup != null && secondaryLocale.equals(secondaryDictionaryGroup.mLocale))
secondaryDictionaryGroup.setMainDict(secondaryMainDict);
else if (secondaryMainDict != null)
secondaryMainDict.close();
}
if (listener != null) {
listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary());
@ -453,13 +567,19 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
public void closeDictionaries() {
final DictionaryGroup dictionaryGroupToClose;
final DictionaryGroup mainDictionaryGroupToClose;
final DictionaryGroup secondaryDictionaryGroupToClose;
synchronized (mLock) {
dictionaryGroupToClose = mDictionaryGroup;
mainDictionaryGroupToClose = mDictionaryGroup;
secondaryDictionaryGroupToClose = mSecondaryDictionaryGroup;
mDictionaryGroup = new DictionaryGroup();
if (mSecondaryDictionaryGroup != null)
mSecondaryDictionaryGroup = new DictionaryGroup();
}
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() {
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();
}
@ -502,11 +627,30 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
putWordIntoValidSpellingWordCache("addToUserHistory", suggestion);
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;
for (int i = 0; i < words.length; i++) {
final String currentWord = words[i];
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,
blockPotentiallyOffensive);
ngramContextForCurrentWord =
@ -542,17 +686,19 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
final int timeStampInSeconds, final boolean blockPotentiallyOffensive) {
final ExpandableBinaryDictionary userHistoryDictionary =
dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY);
if (userHistoryDictionary == null || !isForLocale(userHistoryDictionary.mLocale)) {
if (userHistoryDictionary == null || !hasLocale(userHistoryDictionary.mLocale)) {
return;
}
final int maxFreq = getFrequency(word);
final int maxFreq = getFrequency(word, dictionaryGroup);
if (maxFreq == 0 && blockPotentiallyOffensive) {
return;
}
final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale);
final String secondWord;
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
// 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
@ -560,15 +706,16 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
// 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
// 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.
secondWord = lowerCasedWord;
secondWord = decapitalizedWord;
}
} 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 String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale);
final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN,
null /* account */) ?
dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) :
@ -588,8 +735,18 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
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) {
final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName);
final ExpandableBinaryDictionary dictionary = getCurrentlyPreferredDictionaryGroup().getSubDict(dictName);
if (dictionary != null) {
dictionary.removeUnigramEntryDynamically(word);
}
@ -621,25 +778,70 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
false /* firstSuggestionExceedsConfidenceThreshold */);
final float[] weightOfLangModelVsSpatialModel =
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) {
final Dictionary dictionary = mDictionaryGroup.getDict(dictType);
final Dictionary dictionary = dictGroup.getDict(dictType);
if (null == dictionary) continue;
final float weightForLocale = composedData.mIsBatchMode
? mDictionaryGroup.mWeightForGesturingInLocale
: mDictionaryGroup.mWeightForTypingInLocale;
? dictGroup.mWeightForGesturingInLocale
: dictGroup.mWeightForTypingInLocale;
final ArrayList<SuggestedWordInfo> dictionarySuggestions =
dictionary.getSuggestions(composedData, ngramContext,
proximityInfoHandle, settingsValuesForSuggestion, sessionId,
weightForLocale, weightOfLangModelVsSpatialModel);
if (null == dictionarySuggestions) continue;
suggestionResults.addAll(dictionarySuggestions);
if (null != suggestionResults.mRawSuggestions) {
suggestionResults.mRawSuggestions.addAll(dictionarySuggestions);
}
suggestions.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) {
if (mValidSpellingWordReadCache != null) {
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) {
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)) {
return false;
}
if (mDictionaryGroup.mLocale == null) {
if (dictionaryGroup.mLocale == null) {
return false;
}
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
// would be immutable once it's finished initializing, but concretely a null test is
// probably good enough for the time being.
@ -675,13 +878,18 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
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)) {
return 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) {
final Dictionary dictionary = mDictionaryGroup.getDict(dictType);
final Dictionary dictionary = dictGroup.getDict(dictType);
if (dictionary == null) continue;
final int tempFreq = dictionary.getFrequency(word);
if (tempFreq >= maxFreq) {
@ -697,6 +905,12 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
return false;
}
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;
}

View file

@ -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;
}
}

View file

@ -29,15 +29,18 @@ import android.view.Gravity;
import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager;
import org.dslul.openboard.inputmethod.latin.InputAttributes;
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.utils.AdditionalSubtypeUtils;
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
import org.dslul.openboard.inputmethod.latin.utils.JniUtils;
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
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 java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
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_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 is being used only for the backward compatibility.
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) {
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;
}
}

View file

@ -28,7 +28,6 @@ import org.dslul.openboard.inputmethod.compat.AppWorkaroundsUtils;
import org.dslul.openboard.inputmethod.latin.InputAttributes;
import org.dslul.openboard.inputmethod.latin.R;
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.ResourceUtils;
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils;
@ -86,6 +85,7 @@ public class SettingsValues {
public final long mClipboardHistoryRetentionTime;
public final boolean mOneHandedModeEnabled;
public final int mOneHandedModeGravity;
public final Locale mSecondaryLocale;
// Use bigrams to predict the next word when there is no input for it yet
public final boolean mBigramPredictionEnabled;
public final boolean mGestureInputEnabled;
@ -242,6 +242,7 @@ public class SettingsValues {
mClipboardHistoryRetentionTime = Settings.readClipboardHistoryRetentionTime(prefs, res);
mOneHandedModeEnabled = Settings.readOneHandedModeEnabled(prefs);
mOneHandedModeGravity = Settings.readOneHandedModeGravity(prefs);
mSecondaryLocale = Settings.getSecondaryLocale(prefs, RichInputMethodManager.getInstance().getCurrentSubtypeLocale().toString());
}
public boolean isMetricsLoggingEnabled() {

View file

@ -201,6 +201,9 @@ public class ScriptUtils {
* {@see http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes}
*/
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();
Integer script = mLanguageCodeToScriptCode.get(language);
if (script == null) {

View file

@ -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>
<!-- Preferences item for enabling trackpad space key -->
<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. -->
<string name="space_trackpad_summary">Swipe on the spacebar to move the cursor</string>
<!-- Preferences item for enabling inserting more spaces key -->

View file

@ -82,6 +82,12 @@
android:summary="@string/autospace_after_punctuation_summary"
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>
</PreferenceScreen>