allow multilingual typing with an arbitrary number of languages

currently can't set mor than one extra language, will be enabled in language settings re-work
This commit is contained in:
Helium314 2023-08-11 00:58:33 +02:00
parent e7e05ca1c2
commit 592f4aab0c
5 changed files with 297 additions and 294 deletions

View file

@ -61,6 +61,8 @@ 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.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.WeakHashMap;
@ -844,19 +846,27 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
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 List<Locale> secondaryLocales = Settings.getInstance().getCurrent().mSecondaryLocales;
// avoid showing same language twice
final List<Locale> secondaryLocalesToUse = withoutDuplicateLanguages(secondaryLocales, subtype.getLocale().getLanguage());
if (secondaryLocalesToUse.size() > 0) {
StringBuilder sb = new StringBuilder(subtype.getMiddleDisplayName());
final Locale displayLocale = getResources().getConfiguration().locale;
final String full = subtype.getMiddleDisplayName() + " - " +
secondaryLocale.getDisplayLanguage(displayLocale);
for (Locale locale : secondaryLocales) {
sb.append(" - ");
sb.append(locale.getDisplayLanguage(displayLocale));
}
final String full = sb.toString();
if (fitsTextIntoWidth(width, full, paint)) {
return full;
}
final String middle = subtype.getLocale().getLanguage().toUpperCase(displayLocale) +
" - " + secondaryLocale.getLanguage().toUpperCase(displayLocale);
sb.setLength(0);
sb.append(subtype.getLocale().getLanguage().toUpperCase(displayLocale));
for (Locale locale : secondaryLocales) {
sb.append(" - ");
sb.append(locale.getLanguage().toUpperCase(displayLocale));
}
final String middle = sb.toString();
if (fitsTextIntoWidth(width, middle, paint)) {
return middle;
}
@ -877,6 +887,23 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
return "";
}
private List<Locale> withoutDuplicateLanguages(List<Locale> locales, String mainLanguage) {
ArrayList<String> languages = new ArrayList<String>() {{ add(mainLanguage); }};
ArrayList<Locale> newLocales = new ArrayList<>();
for (Locale locale : locales) {
boolean keep = true;
for (String language : languages) {
if (locale.getLanguage().equals(language))
keep = false;
}
if (!keep)
continue;
languages.add(locale.getLanguage());
newLocales.add(locale);
}
return newLocales;
}
private void drawLanguageOnSpacebar(final Key key, final Canvas canvas, final Paint paint) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) {

View file

@ -76,8 +76,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
// dictionary.
private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140;
private DictionaryGroup mDictionaryGroup = new DictionaryGroup();
private DictionaryGroup mSecondaryDictionaryGroup = new DictionaryGroup();
private ArrayList<DictionaryGroup> mDictionaryGroups = new ArrayList<DictionaryGroup>() {{ add(new DictionaryGroup()); }};
private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0);
// To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
private final Object mLock = new Object();
@ -95,9 +94,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
// todo: 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 -> why? seems they are not needed anyway
private LruCache<String, Boolean> mValidSpellingWordReadCache;
private LruCache<String, Boolean> mValidSpellingWordWriteCache;
@ -113,12 +112,15 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
@Override
public boolean isForLocale(final Locale locale) {
return locale != null && locale.equals(mDictionaryGroup.mLocale);
return locale != null && locale.equals(mDictionaryGroups.get(0).mLocale);
}
private boolean hasLocale(final Locale locale) {
return locale != null && (locale.equals(mDictionaryGroup.mLocale) ||
(mSecondaryDictionaryGroup != null && locale.equals(mSecondaryDictionaryGroup.mLocale)));
if (locale == null) return false;
for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
if (locale.equals(dictionaryGroup.mLocale)) return true;
}
return false;
}
/**
@ -127,7 +129,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
* @param account the account to test against.
*/
public boolean isForAccount(@Nullable final String account) {
return TextUtils.equals(mDictionaryGroup.mAccount, account);
return TextUtils.equals(mDictionaryGroups.get(0).mAccount, account);
}
/**
@ -275,30 +277,27 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
@Override
public void onFinishInput(Context context) {
for (final String dictType : ALL_DICTIONARY_TYPES) {
Dictionary dict = mDictionaryGroup.getDict(dictType);
if (dict != null) dict.onFinishInput();
}
if (mSecondaryDictionaryGroup != null)
for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
for (final String dictType : ALL_DICTIONARY_TYPES) {
Dictionary dict = mSecondaryDictionaryGroup.getDict(dictType);
Dictionary dict = dictionaryGroup.getDict(dictType);
if (dict != null) dict.onFinishInput();
}
}
}
@Override
public boolean isActive() {
return mDictionaryGroup.mLocale != null;
return mDictionaryGroups.get(0).mLocale != null;
}
@Override
public Locale getLocale() {
return mDictionaryGroup.mLocale;
return mDictionaryGroups.get(0).mLocale;
}
@Override
public boolean usesContacts() {
return mDictionaryGroup.getSubDict(Dictionary.TYPE_CONTACTS) != null;
return mDictionaryGroups.get(0).getSubDict(Dictionary.TYPE_CONTACTS) != null;
}
@Override
@ -329,17 +328,19 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
@Nullable
static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup dictionaryGroup,
static DictionaryGroup findDictionaryGroupWithLocale(final List<DictionaryGroup> dictionaryGroups,
final Locale locale) {
if (dictionaryGroup == null) return null;
return locale.equals(dictionaryGroup.mLocale) ? dictionaryGroup : null;
if (dictionaryGroups == null) return null;
for (DictionaryGroup dictionaryGroup : dictionaryGroups) {
if (locale == null && dictionaryGroup.mLocale == null)
return dictionaryGroup;
if (locale != null && locale.equals(dictionaryGroup.mLocale))
return dictionaryGroup;
}
return 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...
// original
public void resetDictionaries(
final Context context,
final Locale newLocale,
@ -353,6 +354,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
// TODO: Make subDictTypesToUse configurable by resource or a static final list.
final HashSet<String> subDictTypesToUse = new HashSet<>();
subDictTypesToUse.add(Dictionary.TYPE_USER);
final List<Locale> allLocales = new ArrayList<Locale>() {{
add(newLocale);
addAll(Settings.getInstance().getCurrent().mSecondaryLocales);
}};
// Do not use contacts dictionary if we do not have permissions to read contacts.
final boolean contactsPermissionGranted = PermissionsUtil.checkAllPermissionsGranted(
@ -364,121 +369,83 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY);
}
// Gather all dictionaries. We'll remove them from the list to clean up later.
final ArrayList<String> dictTypeForLocale = new ArrayList<>();
existingDictionariesToCleanup.put(newLocale, dictTypeForLocale);
final DictionaryGroup currentDictionaryGroupForLocale =
findDictionaryGroupWithLocale(mDictionaryGroup, newLocale);
if (currentDictionaryGroupForLocale != null) {
// Gather all dictionaries by locale. We may remove some from the list to clean up later.
for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
final ArrayList<String> dictTypeForLocale = new ArrayList<>();
existingDictionariesToCleanup.put(dictionaryGroup.mLocale, dictTypeForLocale);
for (final String dictType : DYNAMIC_DICTIONARY_TYPES) {
if (currentDictionaryGroupForLocale.hasDict(dictType, account)) {
if (dictionaryGroup.hasDict(dictType, account)) {
dictTypeForLocale.add(dictType);
}
}
if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) {
if (dictionaryGroup.hasDict(Dictionary.TYPE_MAIN, account)) {
dictTypeForLocale.add(Dictionary.TYPE_MAIN);
}
}
final DictionaryGroup dictionaryGroupForLocale =
findDictionaryGroupWithLocale(mDictionaryGroup, newLocale);
final ArrayList<String> dictTypesToCleanupForLocale =
existingDictionariesToCleanup.get(newLocale);
final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale);
// create new dictionary groups and remove dictionaries to re-use from existingDictionariesToCleanup
final ArrayList<DictionaryGroup> newDictionaryGroups = new ArrayList<>(allLocales.size());
for (Locale locale : allLocales) {
// get existing dictionary group for new locale
final DictionaryGroup oldDictionaryGroupForLocale =
findDictionaryGroupWithLocale(mDictionaryGroups, locale);
final ArrayList<String> dictTypesToCleanupForLocale =
existingDictionariesToCleanup.get(locale);
final boolean noExistingDictsForThisLocale = (null == oldDictionaryGroupForLocale);
final Dictionary mainDict;
if (forceReloadMainDictionary || noExistingDictsForThisLocale
|| !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) {
mainDict = null;
} else {
mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN);
dictTypesToCleanupForLocale.remove(Dictionary.TYPE_MAIN);
}
final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>();
for (final String subDictType : subDictTypesToUse) {
final ExpandableBinaryDictionary subDict;
if (noExistingDictsForThisLocale
|| !dictionaryGroupForLocale.hasDict(subDictType, account)) {
// Create a new dictionary.
subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */,
dictNamePrefix, account);
// create new or re-use already loaded main dict
final Dictionary mainDict;
if (forceReloadMainDictionary || noExistingDictsForThisLocale
|| !oldDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) {
mainDict = null;
} else {
// Reuse the existing dictionary, and don't close it at the end
subDict = dictionaryGroupForLocale.getSubDict(subDictType);
dictTypesToCleanupForLocale.remove(subDictType);
mainDict = oldDictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN);
dictTypesToCleanupForLocale.remove(Dictionary.TYPE_MAIN);
}
subDicts.put(subDictType, subDict);
}
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);
}
}
// create new or re-use already loaded sub-dicts
final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>();
for (final String subDictType : subDictTypesToUse) {
final ExpandableBinaryDictionary subDict =
getSubDict(subDictType, context, secondaryLocale, null, dictNamePrefix, account);
secondarySubDicts.put(subDictType, subDict);
dictTypesToCleanUp.remove(subDictType);
final ExpandableBinaryDictionary subDict;
if (noExistingDictsForThisLocale
|| !oldDictionaryGroupForLocale.hasDict(subDictType, account)) {
// Create a new dictionary.
subDict = getSubDict(subDictType, context, locale, null /* dictFile */, dictNamePrefix, account);
} else {
// Reuse the existing dictionary, and don't close it at the end
subDict = oldDictionaryGroupForLocale.getSubDict(subDictType);
dictTypesToCleanupForLocale.remove(subDictType);
}
subDicts.put(subDictType, subDict);
}
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);
DictionaryGroup newDictGroup = new DictionaryGroup(locale, mainDict, account, subDicts);
newDictionaryGroups.add(newDictGroup);
// 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;
// load blacklist
if (noExistingDictsForThisLocale) {
newDictGroup.blacklistFileName = context.getFilesDir().getAbsolutePath() + File.separator + "blacklists" + File.separator + locale.toString().toLowerCase(Locale.ENGLISH) + ".txt";
if (!new File(newDictGroup.blacklistFileName).exists())
new File(context.getFilesDir().getAbsolutePath() + File.separator + "blacklists").mkdirs();
newDictGroup.blacklist.addAll(readBlacklistFile(newDictGroup.blacklistFileName));
} else {
// re-use if possible
newDictGroup.blacklistFileName = oldDictionaryGroupForLocale.blacklistFileName;
newDictGroup.blacklist.addAll(oldDictionaryGroupForLocale.blacklist);
}
}
// Replace Dictionaries.
final DictionaryGroup oldDictionaryGroup;
final DictionaryGroup oldSecondaryDictionaryGroup;
final List<DictionaryGroup> oldDictionaryGroups;
synchronized (mLock) {
oldDictionaryGroup = mDictionaryGroup;
mDictionaryGroup = newDictionaryGroup;
oldSecondaryDictionaryGroup = mSecondaryDictionaryGroup;
mSecondaryDictionaryGroup = newSecondaryDictionaryGroup;
oldDictionaryGroups = mDictionaryGroups;
mDictionaryGroups = newDictionaryGroups;
if (hasAtLeastOneUninitializedMainDictionary()) {
asyncReloadUninitializedMainDictionaries(context, newLocale,
mSecondaryDictionaryGroup == null ? null : secondaryLocale, listener);
asyncReloadUninitializedMainDictionaries(context, allLocales, listener);
}
}
// load blacklists
mDictionaryGroup.blacklistFileName = context.getFilesDir().getAbsolutePath() + File.separator + "blacklists" + File.separator + newLocale.toString().toLowerCase(Locale.ENGLISH) + ".txt";
if (!new File(mDictionaryGroup.blacklistFileName).exists())
new File(context.getFilesDir().getAbsolutePath() + File.separator + "blacklists").mkdirs();
mDictionaryGroup.blacklist.addAll(readBlacklistFile(mDictionaryGroup.blacklistFileName));
if (mSecondaryDictionaryGroup != null) {
mSecondaryDictionaryGroup.blacklistFileName = context.getFilesDir().getAbsolutePath() + File.separator + "blacklists" + File.separator + secondaryLocale.toString().toLowerCase(Locale.ENGLISH) + ".txt";
if (!new File(mSecondaryDictionaryGroup.blacklistFileName).exists())
new File(context.getFilesDir().getAbsolutePath() + File.separator + "blacklists").mkdirs();
mSecondaryDictionaryGroup.blacklist.addAll(readBlacklistFile(mSecondaryDictionaryGroup.blacklistFileName));
}
if (listener != null) {
listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary());
}
@ -487,13 +454,8 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
for (final Locale localeToCleanUp : existingDictionariesToCleanup.keySet()) {
final ArrayList<String> dictTypesToCleanUp =
existingDictionariesToCleanup.get(localeToCleanUp);
DictionaryGroup dictionarySetToCleanup =
findDictionaryGroupWithLocale(oldDictionaryGroup, localeToCleanUp);
if (dictionarySetToCleanup == null)
dictionarySetToCleanup =
findDictionaryGroupWithLocale(oldSecondaryDictionaryGroup, localeToCleanUp);
if (dictionarySetToCleanup == null)
continue;
final DictionaryGroup dictionarySetToCleanup =
findDictionaryGroupWithLocale(oldDictionaryGroups, localeToCleanUp);
for (final String dictType : dictTypesToCleanUp) {
dictionarySetToCleanup.closeDict(dictType);
}
@ -505,53 +467,52 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
private void asyncReloadUninitializedMainDictionaries(final Context context,
final Locale locale, final Locale secondaryLocale, final DictionaryInitializationListener listener) {
final List<Locale> locales, 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, secondaryLocale, listener, latchForWaitingLoadingMainDictionary);
context, locales, listener, latchForWaitingLoadingMainDictionary);
}
});
}
void doReloadUninitializedMainDictionaries(final Context context, final Locale locale,
final Locale secondaryLocale, final DictionaryInitializationListener listener,
void doReloadUninitializedMainDictionaries(final Context context, final List<Locale> locales,
final DictionaryInitializationListener listener,
final CountDownLatch latchForWaitingLoadingMainDictionary) {
final DictionaryGroup dictionaryGroup =
findDictionaryGroupWithLocale(mDictionaryGroup, locale);
if (null == dictionaryGroup) {
// This should never happen, but better safe than crashy
Log.w(TAG, "Expected a dictionary group for " + locale + " but none found");
return;
final Dictionary[] mainDicts = new Dictionary[locales.size()];
final ArrayList<DictionaryGroup> dictionaryGroups = new ArrayList<>();
for (int i = 0; i < locales.size(); i++) {
Locale locale = locales.get(i);
DictionaryGroup dictionaryGroup = findDictionaryGroupWithLocale(mDictionaryGroups, locale);
if (null == dictionaryGroup) {
// This should never happen, but better safe than crashy
Log.w(TAG, "Expected a dictionary group for " + locale + " but none found");
return;
}
dictionaryGroups.add(dictionaryGroup);
// do nothing if main dict already initialized
if (dictionaryGroup.mMainDict != null && dictionaryGroup.mMainDict.isInitialized()) {
mainDicts[i] = null;
continue;
}
mainDicts[i] = DictionaryFactory.createMainDictionaryFromManager(context, dictionaryGroup.mLocale);
}
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);
} else {
// Dictionary facilitator has been reset for another locale.
mainDict.close();
for (int i = 0; i < locales.size(); i++) {
final Locale locale = locales.get(i);
if (mainDicts[i] == null)
continue;
if (locale.equals(dictionaryGroups.get(i).mLocale)) {
dictionaryGroups.get(i).setMainDict(mainDicts[i]);
} else {
// Dictionary facilitator has been reset for another locale.
mainDicts[i].close();
}
}
if (secondaryDictionaryGroup != null && secondaryLocale.equals(secondaryDictionaryGroup.mLocale))
secondaryDictionaryGroup.setMainDict(secondaryMainDict);
else if (secondaryMainDict != null)
secondaryMainDict.close();
}
if (listener != null) {
listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary());
@ -587,46 +548,45 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
subDicts.put(dictType, dict);
}
}
mDictionaryGroup = new DictionaryGroup(locale, mainDictionary, account, subDicts);
mDictionaryGroups.clear();
mDictionaryGroups.add(new DictionaryGroup(locale, mainDictionary, account, subDicts));
}
public void closeDictionaries() {
final DictionaryGroup mainDictionaryGroupToClose;
final DictionaryGroup secondaryDictionaryGroupToClose;
final ArrayList<DictionaryGroup> dictionaryGroupsToClose;
synchronized (mLock) {
mainDictionaryGroupToClose = mDictionaryGroup;
secondaryDictionaryGroupToClose = mSecondaryDictionaryGroup;
mDictionaryGroup = new DictionaryGroup();
if (mSecondaryDictionaryGroup != null)
mSecondaryDictionaryGroup = new DictionaryGroup();
dictionaryGroupsToClose = new ArrayList<>(mDictionaryGroups);
mDictionaryGroups.clear();
mDictionaryGroups.add(new DictionaryGroup());
}
for (final String dictType : ALL_DICTIONARY_TYPES) {
mainDictionaryGroupToClose.closeDict(dictType);
if (secondaryDictionaryGroupToClose != null)
secondaryDictionaryGroupToClose.closeDict(dictType);
for (DictionaryGroup dictionaryGroup : dictionaryGroupsToClose) {
for (final String dictType : ALL_DICTIONARY_TYPES) {
dictionaryGroup.closeDict(dictType);
}
}
}
@UsedForTesting
public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) {
return mDictionaryGroup.getSubDict(dictName);
return mDictionaryGroups.get(0).getSubDict(dictName);
}
// The main dictionaries are loaded asynchronously. Don't cache the return value
// of these methods.
public boolean hasAtLeastOneInitializedMainDictionary() {
final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN);
return mainDict != null && mainDict.isInitialized();
for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN);
if (mainDict != null && mainDict.isInitialized()) return true;
}
return false;
}
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;
for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN);
if (mainDict == null || !mainDict.isInitialized()) return true;
}
return mainDict == null || !mainDict.isInitialized();
return false;
}
public void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit)
@ -638,7 +598,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit)
throws InterruptedException {
waitForLoadingMainDictionaries(timeout, unit);
for (final ExpandableBinaryDictionary dict : mDictionaryGroup.mSubDictMap.values()) {
for (final ExpandableBinaryDictionary dict : mDictionaryGroups.get(0).mSubDictMap.values()) {
dict.waitAllTasksForTests();
}
}
@ -652,36 +612,33 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
final String[] words = suggestion.split(Constants.WORD_SEPARATOR);
// increase / decrease confidence if we have a secondary dictionary group
Boolean validMainWord = null;
Boolean validSecondaryWord = null;
if (mSecondaryDictionaryGroup != null && words.length == 1) {
// increase / decrease confidence if we have more than one dictionary group
boolean[] validWordForDictionary; // store results to avoid unnecessary duplicate lookups
if (mDictionaryGroups.size() > 1 && words.length == 1) {
validWordForDictionary = new boolean[mDictionaryGroups.size()];
// 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;
validMainWord = isValidWord(suggestion, ALL_DICTIONARY_TYPES, mDictionaryGroup);
if ((wasAutoCapitalized && isValidWord(decapitalizedSuggestion, ALL_DICTIONARY_TYPES, mDictionaryGroup))
|| validMainWord)
mDictionaryGroup.increaseConfidence();
else mDictionaryGroup.decreaseConfidence();
validSecondaryWord = isValidWord(suggestion, ALL_DICTIONARY_TYPES, mSecondaryDictionaryGroup);
if ((wasAutoCapitalized && isValidWord(decapitalizedSuggestion, ALL_DICTIONARY_TYPES, mSecondaryDictionaryGroup))
|| validSecondaryWord)
mSecondaryDictionaryGroup.increaseConfidence();
else mSecondaryDictionaryGroup.decreaseConfidence();
}
for (int i = 0; i < mDictionaryGroups.size(); i ++) {
final DictionaryGroup dictionaryGroup = mDictionaryGroups.get(i);
final boolean isValidWord = isValidWord(suggestion, ALL_DICTIONARY_TYPES, dictionaryGroup);
if (isValidWord || (wasAutoCapitalized && isValidWord(decapitalizedSuggestion, ALL_DICTIONARY_TYPES, dictionaryGroup)))
dictionaryGroup.increaseConfidence();
else dictionaryGroup.decreaseConfidence();
validWordForDictionary[i] = isValidWord;
}
} else
validWordForDictionary = null;
// add word to user dictionary if it is in no other dictionary except user history dictionary
// add word to user dictionary if it is in no other dictionary except user history dictionary,
// reasoning: typing the same word again -> we probably want it in some dictionary permanently
// we need a clearly preferred group to assign it to the correct language (in most cases at least...)
if (mDictionaryGroup.hasDict(Dictionary.TYPE_USER_HISTORY, mDictionaryGroup.mAccount) // disable if personalized suggestions are off
&& Settings.getInstance().getCurrent().mAddToPersonalDictionary
&& (mSecondaryDictionaryGroup == null || mDictionaryGroup.mConfidence != mSecondaryDictionaryGroup.mConfidence)
if (mDictionaryGroups.get(0).hasDict(Dictionary.TYPE_USER_HISTORY, mDictionaryGroups.get(0).mAccount) // require personalized suggestions to be on
&& Settings.getInstance().getCurrent().mAddToPersonalDictionary // ...and the setting
&& !wasAutoCapitalized && words.length == 1) {
addToPersonalDictionaryIfInvalidButInHistory(suggestion, validMainWord, validSecondaryWord);
addToPersonalDictionaryIfInvalidButInHistory(suggestion, validWordForDictionary);
}
NgramContext ngramContextForCurrentWord = ngramContext;
@ -695,35 +652,44 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
ngramContextForCurrentWord =
ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord));
// remove entered words from blacklist
if (mDictionaryGroup.blacklist.remove(currentWord))
removeWordFromBlacklistFile(currentWord, mDictionaryGroup.blacklistFileName);
if (mSecondaryDictionaryGroup != null && mSecondaryDictionaryGroup.blacklist.remove(currentWord))
removeWordFromBlacklistFile(currentWord, mSecondaryDictionaryGroup.blacklistFileName);
// remove manually entered blacklisted words from blacklist
for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
if (dictionaryGroup.blacklist.remove(currentWord))
removeWordFromBlacklistFile(currentWord, dictionaryGroup.blacklistFileName);
}
}
}
// main and secondary isValid provided to avoid duplicate lookups
private void addToPersonalDictionaryIfInvalidButInHistory(String suggestion, Boolean validMainWord, Boolean validSecondaryWord) {
private void addToPersonalDictionaryIfInvalidButInHistory(String suggestion, boolean[] validWordForDictionary) {
// we need one clearly preferred group to assign it to the correct language
int highestGroup = -1;
int highestGroupConfidence = -1;
for (int i = 0; i < mDictionaryGroups.size(); i ++) {
final DictionaryGroup dictionaryGroup = mDictionaryGroups.get(i);
if (dictionaryGroup.mConfidence > highestGroupConfidence) {
highestGroup = i;
highestGroupConfidence = dictionaryGroup.mConfidence;
} else if (dictionaryGroup.mConfidence == highestGroupConfidence) {
highestGroup = -1;
}
}
// no preferred group or word is valid -> do nothing
if (highestGroup == -1) return;
final DictionaryGroup dictionaryGroup = mDictionaryGroups.get(highestGroup);
if (validWordForDictionary == null
? isValidWord(suggestion, ALL_DICTIONARY_TYPES, dictionaryGroup)
: validWordForDictionary[highestGroup]
)
return;
final ExpandableBinaryDictionary userDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER);
final Dictionary userHistoryDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY);
// user history always reports words as invalid, so here we need to check isInDictionary instead
// also maybe a problem: words added to dictionaries (user and history) are apparently found
// only after some delay. but this is not too bad, it just delays adding
final DictionaryGroup dictionaryGroup = getCurrentlyPreferredDictionaryGroup();
final ExpandableBinaryDictionary userDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER);
final Dictionary userHistoryDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY);
if (userDict != null && userHistoryDict.isInDictionary(suggestion)) {
if (validMainWord == null)
validMainWord = isValidWord(suggestion, ALL_DICTIONARY_TYPES, mDictionaryGroup);
if (validMainWord)
return;
if (mSecondaryDictionaryGroup != null) {
if (validSecondaryWord == null)
validSecondaryWord = isValidWord(suggestion, ALL_DICTIONARY_TYPES, mSecondaryDictionaryGroup);
if (validSecondaryWord)
return;
}
if (userDict.isInDictionary(suggestion))
if (userDict.isInDictionary(suggestion)) // is this check necessary?
return;
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
@Override
@ -813,13 +779,16 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
isValid, timeStampInSeconds);
}
/** returns the dictionaryGroup with most confidence, main group when tied */
/** returns the dictionaryGroup with most confidence, first group when tied */
private DictionaryGroup getCurrentlyPreferredDictionaryGroup() {
final DictionaryGroup dictGroup;
if (mSecondaryDictionaryGroup == null || mSecondaryDictionaryGroup.mConfidence <= mDictionaryGroup.mConfidence)
dictGroup = mDictionaryGroup;
else
dictGroup = mSecondaryDictionaryGroup;
DictionaryGroup dictGroup = null;
int highestConfidence = -1;
for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
if (dictionaryGroup.mConfidence > highestConfidence) {
dictGroup = dictionaryGroup;
highestConfidence = dictGroup.mConfidence;
}
}
return dictGroup;
}
@ -857,39 +826,47 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
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();
}
});
}
// start getting suggestions for non-main locales first, but in background
final ArrayList<SuggestedWordInfo>[] otherDictionarySuggestions = new ArrayList[mDictionaryGroups.size() - 1];
final CountDownLatch waitForOtherDictionaries;
if (mDictionaryGroups.size() > 1) {
waitForOtherDictionaries = new CountDownLatch(mDictionaryGroups.size() - 1);
for (int i = 1; i < mDictionaryGroups.size(); i ++) {
final DictionaryGroup dictionaryGroup = mDictionaryGroups.get(i);
final int index = i - 1;
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
@Override
public void run() {
otherDictionarySuggestions[index] = getSuggestions(composedData,
ngramContext, settingsValuesForSuggestion, sessionId, proximityInfoHandle,
weightOfLangModelVsSpatialModel, dictionaryGroup);
waitForOtherDictionaries.countDown();
}
});
}
} else
waitForOtherDictionaries = null;
// get main locale suggestions
final ArrayList<SuggestedWordInfo> dictionarySuggestions = getSuggestions(composedData,
ngramContext, settingsValuesForSuggestion, sessionId, proximityInfoHandle,
weightOfLangModelVsSpatialModel, mDictionaryGroup);
weightOfLangModelVsSpatialModel, mDictionaryGroups.get(0));
suggestionResults.addAll(dictionarySuggestions);
if (null != suggestionResults.mRawSuggestions) {
suggestionResults.mRawSuggestions.addAll(dictionarySuggestions);
}
// wait for secondary locale suggestions
if (mSecondaryDictionaryGroup != null) {
try { waitForSecondaryDictionary.await(); }
// wait for other locale suggestions
if (waitForOtherDictionaries != null) {
try { waitForOtherDictionaries.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);
for (int i = 1; i < mDictionaryGroups.size(); i ++) {
suggestionResults.addAll(otherDictionarySuggestions[i - 1]);
if (null != suggestionResults.mRawSuggestions) {
suggestionResults.mRawSuggestions.addAll(otherDictionarySuggestions[i - 1]);
}
}
}
@ -938,13 +915,16 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
return cachedValue;
}
}
return isValidWord(word, ALL_DICTIONARY_TYPES, mDictionaryGroup) ||
(mSecondaryDictionaryGroup != null && isValidWord(word, ALL_DICTIONARY_TYPES, mSecondaryDictionaryGroup));
for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
if (isValidWord(word, ALL_DICTIONARY_TYPES, dictionaryGroup))
return true;
}
return false;
}
// this is unused, so leave it for now (redirecting to isValidWord seems to defeat the purpose...)
public boolean isValidSuggestionWord(final String word) {
return isValidWord(word, ALL_DICTIONARY_TYPES, mDictionaryGroup);
return isValidWord(word, ALL_DICTIONARY_TYPES, mDictionaryGroups.get(0));
}
private boolean isValidWord(final String word, final String[] dictionariesToCheck, final DictionaryGroup dictionaryGroup) {
@ -969,18 +949,18 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
private boolean isBlacklisted(final String word) {
if (mDictionaryGroup.blacklist.contains(word))
return true;
if (mSecondaryDictionaryGroup != null && mSecondaryDictionaryGroup.blacklist.contains(word))
return true;
for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
if (dictionaryGroup.blacklist.contains(word))
return true;
}
return false;
}
@Override
public void removeWord(String word) {
removeWordFromGroup(word, mDictionaryGroup);
if (mSecondaryDictionaryGroup != null)
removeWordFromGroup(word, mSecondaryDictionaryGroup);
for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
removeWordFromGroup(word, dictionaryGroup);
}
}
private void removeWordFromGroup(String word, DictionaryGroup group) {
@ -1071,16 +1051,12 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
private boolean clearSubDictionary(final String dictName) {
final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName);
if (dictionary == null) {
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();
for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictName);
if (dictionary == null) {
return false; // should only ever happen for primary dictionary, so this is safe
}
dictionary.clear();
}
return true;
}
@ -1092,7 +1068,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
@Override
public void dumpDictionaryForDebug(final String dictName) {
final ExpandableBinaryDictionary dictToDump = mDictionaryGroup.getSubDict(dictName);
final ExpandableBinaryDictionary dictToDump = mDictionaryGroups.get(0).getSubDict(dictName);
if (dictToDump == null) {
Log.e(TAG, "Cannot dump " + dictName + ". "
+ "The dictionary is not being used for suggestion or cannot be dumped.");
@ -1102,10 +1078,11 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
@Override
// this is unused, so leave it for now
@Nonnull public List<DictionaryStats> getDictionaryStats(final Context context) {
final ArrayList<DictionaryStats> statsOfEnabledSubDicts = new ArrayList<>();
for (final String dictType : DYNAMIC_DICTIONARY_TYPES) {
final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictType);
final ExpandableBinaryDictionary dictionary = mDictionaryGroups.get(0).getSubDict(dictType);
if (dictionary == null) continue;
statsOfEnabledSubDicts.add(dictionary.getDictionaryStats());
}

View file

@ -44,7 +44,8 @@ public final class SecondaryLocaleSettingsFragment extends SubScreenFragment {
List<InputMethodSubtype> subtypes = mRichImm.getMyEnabledInputMethodSubtypeList(true);
for (InputMethodSubtype subtype : subtypes) {
final Locale secondaryLocale = Settings.getSecondaryLocale(getSharedPreferences(), subtype.getLocale());
final List<Locale> secondaryLocales = Settings.getSecondaryLocales(getSharedPreferences(), subtype.getLocale());
final Locale secondaryLocale = secondaryLocales.size() > 0 ? secondaryLocales.get(0) : null;
final Preference pref = new Preference(context);
pref.setTitle(subtype.getDisplayName(context, BuildConfig.APPLICATION_ID, context.getApplicationInfo()));
if (secondaryLocale != null)
@ -94,7 +95,8 @@ public final class SecondaryLocaleSettingsFragment extends SubScreenFragment {
titles[i] = loc.getDisplayName(displayLocale);
}
Locale currentSecondaryLocale = Settings.getSecondaryLocale(getSharedPreferences(), mainLocale);
final List<Locale> secondaryLocales = Settings.getSecondaryLocales(getSharedPreferences(), mainLocale);
final Locale currentSecondaryLocale = secondaryLocales.size() > 0 ? secondaryLocales.get(0) : null;
int checkedItem;
if (currentSecondaryLocale == null)
checkedItem = 0;

View file

@ -22,14 +22,11 @@ import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Build;
import android.util.Log;
import android.view.Gravity;
import androidx.core.content.ContextCompat;
import org.dslul.openboard.inputmethod.keyboard.KeyboardTheme;
import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager;
import org.dslul.openboard.inputmethod.latin.InputAttributes;
@ -44,8 +41,10 @@ import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
import org.dslul.openboard.inputmethod.latin.utils.RunInLocale;
import org.dslul.openboard.inputmethod.latin.utils.StatsUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
@ -537,14 +536,15 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return prefs.getInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID, defValue);
}
public static Locale getSecondaryLocale(final SharedPreferences prefs, final String mainLocaleString) {
// todo: adjust for multiple secondary locales
public static List<Locale> getSecondaryLocales(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 new ArrayList<Locale>() {{ add(LocaleUtils.constructLocaleFromString(locales[1])); }};
}
return null;
return new ArrayList<>();
}
public static Colors getColors(final Context context, final SharedPreferences prefs) {

View file

@ -21,13 +21,9 @@ import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.ColorFilter;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
import androidx.core.graphics.BlendModeColorFilterCompat;
import androidx.core.graphics.BlendModeCompat;
import org.dslul.openboard.inputmethod.compat.AppWorkaroundsUtils;
import org.dslul.openboard.inputmethod.latin.InputAttributes;
import org.dslul.openboard.inputmethod.latin.R;
@ -40,6 +36,7 @@ import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils;
import org.dslul.openboard.inputmethod.latin.utils.TargetPackageInfoGetterTask;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.annotation.Nonnull;
@ -91,7 +88,7 @@ public class SettingsValues {
public final long mClipboardHistoryRetentionTime;
public final boolean mOneHandedModeEnabled;
public final int mOneHandedModeGravity;
public final Locale mSecondaryLocale;
public final List<Locale> mSecondaryLocales;
// Use bigrams to predict the next word when there is no input for it yet
public final boolean mBigramPredictionEnabled;
public final boolean mGestureInputEnabled;
@ -254,7 +251,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());
mSecondaryLocales = Settings.getSecondaryLocales(prefs, RichInputMethodManager.getInstance().getCurrentSubtypeLocale().toString());
mColors = Settings.getColors(context, prefs);
mColors.createColorFilters(prefs.getBoolean(Settings.PREF_THEME_KEY_BORDERS, false));