getDictionaryStats(final Context context);
diff --git a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.java b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.java
new file mode 100644
index 000000000..e075b1290
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.java
@@ -0,0 +1,1085 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.latin;
+
+import android.Manifest;
+import android.content.Context;
+import android.provider.UserDictionary;
+import android.text.TextUtils;
+import android.util.LruCache;
+import android.view.inputmethod.InputMethodSubtype;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import helium314.keyboard.keyboard.Keyboard;
+import helium314.keyboard.latin.NgramContext.WordInfo;
+import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
+import helium314.keyboard.latin.common.ComposedData;
+import helium314.keyboard.latin.common.Constants;
+import helium314.keyboard.latin.common.StringUtils;
+import helium314.keyboard.latin.common.StringUtilsKt;
+import helium314.keyboard.latin.permissions.PermissionsUtil;
+import helium314.keyboard.latin.personalization.UserHistoryDictionary;
+import helium314.keyboard.latin.settings.Settings;
+import helium314.keyboard.latin.settings.SettingsValues;
+import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
+import helium314.keyboard.latin.utils.ExecutorUtils;
+import helium314.keyboard.latin.utils.KtxKt;
+import helium314.keyboard.latin.utils.Log;
+import helium314.keyboard.latin.utils.SubtypeSettings;
+import helium314.keyboard.latin.utils.SubtypeUtilsKt;
+import helium314.keyboard.latin.utils.SuggestionResults;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Facilitates interaction with different kinds of dictionaries. Provides APIs
+ * to instantiate and select the correct dictionaries (based on language or account),
+ * update entries and fetch suggestions.
+ *
+ * Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as
+ * a client for interacting with dictionaries.
+ */
+public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
+ public static final String TAG = DictionaryFacilitatorImpl.class.getSimpleName();
+
+ // HACK: This threshold is being used when adding a capitalized entry in the User History
+ // dictionary.
+ private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140;
+
+ private ArrayList mDictionaryGroups = new ArrayList<>() {{ add(new DictionaryGroup()); }};
+ private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0);
+ // To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
+ private final Object mLock = new Object();
+ // library does not deal well with ngram history for auto-capitalized words, so we adjust the ngram
+ // context to store next word suggestions for such cases
+ private boolean mTryChangingWords = false;
+ private String mChangeFrom = "";
+ private String mChangeTo = "";
+
+ // todo: write cache never set, and never read (only written)
+ // tried to use read cache for a while, but small performance improvements are not worth the work (https://github.com/Helium314/HeliBoard/issues/307)
+ private LruCache mValidSpellingWordReadCache;
+ private LruCache mValidSpellingWordWriteCache;
+
+ @Override
+ public void setValidSpellingWordReadCache(final LruCache cache) {
+ mValidSpellingWordReadCache = cache;
+ }
+
+ @Override
+ public void setValidSpellingWordWriteCache(final LruCache cache) {
+ mValidSpellingWordWriteCache = cache;
+ }
+
+ // judging by usage, this should check primary locale only
+ @Override
+ public boolean isForLocale(final Locale locale) {
+ return locale != null && locale.equals(mDictionaryGroups.get(0).mLocale);
+ }
+
+ private boolean hasLocale(final Locale locale) {
+ if (locale == null) return false;
+ for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
+ if (locale.equals(dictionaryGroup.mLocale)) return true;
+ }
+ return false;
+ }
+
+ /**
+ * A group of dictionaries that work together for a single language.
+ */
+ private static class DictionaryGroup {
+ private static final int MAX_CONFIDENCE = 2;
+
+ /**
+ * The locale associated with the dictionary group.
+ */
+ @NonNull public final Locale mLocale;
+
+ /**
+ * The user account associated with the dictionary group.
+ */
+ @Nullable public final String mAccount;
+
+ @Nullable private Dictionary mMainDict;
+ // Confidence that the most probable language is actually the language the user is
+ // typing in. For now, this is simply the number of times a word from this language
+ // has been committed in a row, with an exception when typing a single word not contained
+ // in this language.
+ private int mConfidence = 1;
+
+ // words cannot be removed from main dictionary, so we use a blacklist instead
+ public String blacklistFileName = null;
+ public Set blacklist = new HashSet<>();
+
+ // 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 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 > 0) {
+ mConfidence -= 1;
+ }
+ }
+
+ public float getWeightForTypingInLocale(List groups) {
+ return getWeightForLocale(groups, 0.15f);
+ }
+
+ public float getWeightForGesturingInLocale(List groups) {
+ return getWeightForLocale(groups, 0.05f);
+ }
+
+ // might need some more tuning
+ private float getWeightForLocale(final List groups, final float step) {
+ if (groups.size() == 1) return 1f;
+ if (mConfidence < 2) return 1f - step * (MAX_CONFIDENCE - mConfidence);
+ for (DictionaryGroup group : groups) {
+ if (group != this && group.mConfidence >= mConfidence) return 1f - step / 2f;
+ }
+ return 1f;
+ }
+ public final ConcurrentHashMap mSubDictMap =
+ new ConcurrentHashMap<>();
+
+ public DictionaryGroup() {
+ this(new Locale(""), null, null, Collections.emptyMap());
+ }
+
+ public DictionaryGroup(@NonNull final Locale locale,
+ @Nullable final Dictionary mainDict,
+ @Nullable final String account,
+ @NonNull final Map subDicts) {
+ mLocale = locale;
+ mAccount = account;
+ // The main dictionary can be asynchronously loaded.
+ setMainDict(mainDict);
+ for (final Map.Entry entry : subDicts.entrySet()) {
+ setSubDict(entry.getKey(), entry.getValue());
+ }
+ }
+
+ private void setSubDict(@NonNull final String dictType, @NonNull final ExpandableBinaryDictionary dict) {
+ mSubDictMap.put(dictType, dict);
+ }
+
+ public void setMainDict(@Nullable final Dictionary mainDict) {
+ // Close old dictionary if exists. Main dictionary can be assigned multiple times.
+ final Dictionary oldDict = mMainDict;
+ mMainDict = mainDict;
+ if (oldDict != null && mainDict != oldDict) {
+ oldDict.close();
+ }
+ }
+
+ public @Nullable Dictionary getDict(@NonNull final String dictType) {
+ if (Dictionary.TYPE_MAIN.equals(dictType)) {
+ return mMainDict;
+ }
+ return getSubDict(dictType);
+ }
+
+ public @Nullable ExpandableBinaryDictionary getSubDict(@NonNull final String dictType) {
+ return mSubDictMap.get(dictType);
+ }
+
+ public boolean hasDict(@NonNull final String dictType, @Nullable final String account) {
+ if (Dictionary.TYPE_MAIN.equals(dictType)) {
+ return mMainDict != null;
+ }
+ if (Dictionary.TYPE_USER_HISTORY.equals(dictType) &&
+ !TextUtils.equals(account, mAccount)) {
+ // If the dictionary type is user history, & if the account doesn't match,
+ // return immediately. If the account matches, continue looking it up in the
+ // sub dictionary map.
+ return false;
+ }
+ return mSubDictMap.containsKey(dictType);
+ }
+
+ public void closeDict(@NonNull final String dictType) {
+ final Dictionary dict;
+ if (Dictionary.TYPE_MAIN.equals(dictType)) {
+ dict = mMainDict;
+ } else {
+ dict = mSubDictMap.remove(dictType);
+ }
+ if (dict != null) {
+ dict.close();
+ }
+ }
+ }
+
+ public DictionaryFacilitatorImpl() {
+ }
+
+ @Override
+ public void onStartInput() {
+ }
+
+ @Override
+ public void onFinishInput(Context context) {
+ for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
+ for (final String dictType : ALL_DICTIONARY_TYPES) {
+ Dictionary dict = dictionaryGroup.getDict(dictType);
+ if (dict != null) dict.onFinishInput();
+ }
+ }
+ }
+
+ @Override
+ public boolean isActive() {
+ return !mDictionaryGroups.get(0).mLocale.getLanguage().isEmpty();
+ }
+
+ @Override
+ @NonNull
+ public Locale getMainLocale() {
+ return mDictionaryGroups.get(0).mLocale;
+ }
+
+ @Override
+ public Locale getCurrentLocale() {
+ return getCurrentlyPreferredDictionaryGroup().mLocale;
+ }
+
+ public boolean usesContacts() {
+ return mDictionaryGroups.get(0).getSubDict(Dictionary.TYPE_CONTACTS) != null;
+ }
+
+ public boolean usesPersonalization() {
+ return mDictionaryGroups.get(0).getSubDict(Dictionary.TYPE_USER_HISTORY) != null;
+ }
+
+ @Override
+ public String getAccount() {
+ return null;
+ }
+
+ @Override
+ public boolean usesSameSettings(@NonNull final List locales, final boolean contacts,
+ final boolean personalization, @Nullable final String account) {
+ final boolean first = usesContacts() == contacts && usesPersonalization() == personalization
+ && TextUtils.equals(mDictionaryGroups.get(0).mAccount, account)
+ && locales.size() == mDictionaryGroups.size();
+ if (!first) return false;
+ for (int i = 0; i < locales.size(); i++) {
+ if (locales.get(i) != mDictionaryGroups.get(i).mLocale) return false;
+ }
+ return true;
+ }
+
+ @Nullable
+ private static ExpandableBinaryDictionary getSubDict(final String dictType,
+ final Context context, final Locale locale, final File dictFile,
+ final String dictNamePrefix, @Nullable final String account) {
+ ExpandableBinaryDictionary dict = null;
+ try {
+ dict = switch (dictType) {
+ case Dictionary.TYPE_USER_HISTORY -> UserHistoryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix, account);
+ case Dictionary.TYPE_USER -> UserBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix, account);
+ case Dictionary.TYPE_CONTACTS -> ContactsBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix, account);
+ default -> null;
+ };
+ } catch (final SecurityException | IllegalArgumentException e) {
+ Log.e(TAG, "Cannot create dictionary: " + dictType, e);
+ }
+ if (dict == null)
+ Log.e(TAG, "Cannot create dictionary for " + dictType);
+ return dict;
+ }
+
+ @Nullable
+ static DictionaryGroup findDictionaryGroupWithLocale(final List dictionaryGroups,
+ @NonNull final Locale locale) {
+ if (dictionaryGroups == null) return null;
+ for (DictionaryGroup dictionaryGroup : dictionaryGroups) {
+ if (locale.equals(dictionaryGroup.mLocale))
+ return dictionaryGroup;
+ }
+ return null;
+ }
+
+ // original
+ public void resetDictionaries(
+ final Context context,
+ @NonNull final Locale newLocale,
+ final boolean useContactsDict,
+ final boolean usePersonalizedDicts,
+ final boolean forceReloadMainDictionary,
+ @Nullable final String account,
+ final String dictNamePrefix,
+ @Nullable final DictionaryInitializationListener listener) {
+ final HashMap> existingDictionariesToCleanup = new HashMap<>();
+ final HashSet subDictTypesToUse = new HashSet<>();
+ subDictTypesToUse.add(Dictionary.TYPE_USER);
+ Log.i(TAG, "resetDictionaries, force reloading main dictionary: " + forceReloadMainDictionary);
+ final List allLocales = new ArrayList<>() {{
+ add(newLocale);
+
+ // adding secondary locales is a bit tricky since they depend on the subtype
+ // but usually this is called with the selected subtype locale
+ final InputMethodSubtype selected = SubtypeSettings.INSTANCE.getSelectedSubtype(KtxKt.prefs(context));
+ if (SubtypeUtilsKt.locale(selected).equals(newLocale)) {
+ addAll(SubtypeUtilsKt.getSecondaryLocales(selected.getExtraValue()));
+ } else {
+ // probably we're called from the spell checker when using a different app as keyboard
+ final List enabled = SubtypeSettings.INSTANCE.getEnabledSubtypes(false);
+ for (InputMethodSubtype subtype : enabled) {
+ if (SubtypeUtilsKt.locale(subtype).equals(newLocale))
+ addAll(SubtypeUtilsKt.getSecondaryLocales(subtype.getExtraValue()));
+ }
+ }
+ }};
+
+ // Do not use contacts dictionary if we do not have permissions to read contacts.
+ if (useContactsDict
+ && PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS)) {
+ subDictTypesToUse.add(Dictionary.TYPE_CONTACTS);
+ }
+ if (usePersonalizedDicts) {
+ subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY);
+ }
+
+ // Gather all dictionaries by locale. We may remove some from the list to clean up later.
+ for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
+ final ArrayList dictTypeForLocale = new ArrayList<>();
+ existingDictionariesToCleanup.put(dictionaryGroup.mLocale, dictTypeForLocale);
+
+ for (final String dictType : DYNAMIC_DICTIONARY_TYPES) {
+ if (dictionaryGroup.hasDict(dictType, account)) {
+ dictTypeForLocale.add(dictType);
+ }
+ }
+ if (dictionaryGroup.hasDict(Dictionary.TYPE_MAIN, account)) {
+ dictTypeForLocale.add(Dictionary.TYPE_MAIN);
+ }
+ }
+
+ // create new dictionary groups and remove dictionaries to re-use from existingDictionariesToCleanup
+ final ArrayList newDictionaryGroups = new ArrayList<>(allLocales.size());
+ for (Locale locale : allLocales) {
+ // get existing dictionary group for new locale
+ final DictionaryGroup oldDictionaryGroupForLocale = findDictionaryGroupWithLocale(mDictionaryGroups, locale);
+ final ArrayList dictTypesToCleanupForLocale = existingDictionariesToCleanup.get(locale);
+ final boolean noExistingDictsForThisLocale = (null == oldDictionaryGroupForLocale);
+
+ // create new or re-use already loaded main dict
+ final Dictionary mainDict;
+ if (forceReloadMainDictionary || noExistingDictsForThisLocale
+ || !oldDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) {
+ mainDict = null;
+ } else {
+ mainDict = oldDictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN);
+ dictTypesToCleanupForLocale.remove(Dictionary.TYPE_MAIN);
+ }
+
+ // create new or re-use already loaded sub-dicts
+ final Map subDicts = new HashMap<>();
+ for (final String subDictType : subDictTypesToUse) {
+ final ExpandableBinaryDictionary subDict;
+ if (noExistingDictsForThisLocale || forceReloadMainDictionary
+ || !oldDictionaryGroupForLocale.hasDict(subDictType, account)) {
+ // Create a new dictionary.
+ subDict = getSubDict(subDictType, context, locale, null, dictNamePrefix, account);
+ if (subDict == null) continue; // https://github.com/Helium314/HeliBoard/issues/293
+ } else {
+ // Reuse the existing dictionary, and don't close it at the end
+ subDict = oldDictionaryGroupForLocale.getSubDict(subDictType);
+ dictTypesToCleanupForLocale.remove(subDictType);
+ }
+ subDicts.put(subDictType, subDict);
+ }
+ DictionaryGroup newDictGroup = new DictionaryGroup(locale, mainDict, account, subDicts);
+ newDictionaryGroups.add(newDictGroup);
+
+ // load blacklist
+ if (noExistingDictsForThisLocale) {
+ newDictGroup.blacklistFileName = context.getFilesDir().getAbsolutePath() + File.separator + "blacklists" + File.separator + locale.toLanguageTag() + ".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 List oldDictionaryGroups;
+ synchronized (mLock) {
+ oldDictionaryGroups = mDictionaryGroups;
+ mDictionaryGroups = newDictionaryGroups;
+ if (hasAtLeastOneUninitializedMainDictionary()) {
+ asyncReloadUninitializedMainDictionaries(context, allLocales, listener);
+ }
+ }
+
+ if (listener != null) {
+ listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary());
+ }
+
+ // Clean up old dictionaries.
+ for (final Locale localeToCleanUp : existingDictionariesToCleanup.keySet()) {
+ final ArrayList dictTypesToCleanUp = existingDictionariesToCleanup.get(localeToCleanUp);
+ final DictionaryGroup dictionarySetToCleanup = findDictionaryGroupWithLocale(oldDictionaryGroups, localeToCleanUp);
+ for (final String dictType : dictTypesToCleanUp) {
+ dictionarySetToCleanup.closeDict(dictType);
+ }
+ }
+
+ if (mValidSpellingWordWriteCache != null) {
+ mValidSpellingWordWriteCache.evictAll();
+ }
+ if (mValidSpellingWordReadCache != null) {
+ mValidSpellingWordReadCache.evictAll();
+ }
+ }
+
+ private void asyncReloadUninitializedMainDictionaries(final Context context,
+ final List locales, final DictionaryInitializationListener listener) {
+ final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1);
+ mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary;
+ ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(() ->
+ doReloadUninitializedMainDictionaries(context, locales, listener, latchForWaitingLoadingMainDictionary));
+ }
+
+ void doReloadUninitializedMainDictionaries(final Context context, final List locales,
+ final DictionaryInitializationListener listener,
+ final CountDownLatch latchForWaitingLoadingMainDictionary) {
+ final Dictionary[] mainDicts = new Dictionary[locales.size()];
+ final ArrayList 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] = DictionaryFactoryKt.createMainDictionary(context, dictionaryGroup.mLocale);
+ }
+
+ synchronized (mLock) {
+ 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 (listener != null) {
+ listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary());
+ }
+ latchForWaitingLoadingMainDictionary.countDown();
+ }
+
+ public void closeDictionaries() {
+ final ArrayList dictionaryGroupsToClose;
+ synchronized (mLock) {
+ dictionaryGroupsToClose = new ArrayList<>(mDictionaryGroups);
+ mDictionaryGroups.clear();
+ mDictionaryGroups.add(new DictionaryGroup());
+ }
+ for (DictionaryGroup dictionaryGroup : dictionaryGroupsToClose) {
+ for (final String dictType : ALL_DICTIONARY_TYPES) {
+ dictionaryGroup.closeDict(dictType);
+ }
+ }
+ }
+
+ // The main dictionaries are loaded asynchronously. Don't cache the return value
+ // of these methods.
+ public boolean hasAtLeastOneInitializedMainDictionary() {
+ for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
+ final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN);
+ if (mainDict != null && mainDict.isInitialized()) return true;
+ }
+ return false;
+ }
+
+ public boolean hasAtLeastOneUninitializedMainDictionary() {
+ for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
+ final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN);
+ if (mainDict == null || !mainDict.isInitialized()) return true;
+ }
+ return false;
+ }
+
+ public void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit)
+ throws InterruptedException {
+ mLatchForWaitingLoadingMainDictionaries.await(timeout, unit);
+ }
+
+ public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized,
+ @NonNull final NgramContext ngramContext, final long timeStampInSeconds,
+ final boolean blockPotentiallyOffensive) {
+ // Update the spelling cache before learning. Words that are not yet added to user history
+ // and appear in no other language model are not considered valid.
+ putWordIntoValidSpellingWordCache("addToUserHistory", suggestion);
+
+ final String[] words = suggestion.split(Constants.WORD_SEPARATOR);
+
+ // 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) { // ignore if more than a single word, this only happens with (badly working) spaceAwareGesture
+ validWordForDictionary = adjustConfidencesInternal(suggestion, wasAutoCapitalized);
+ } else
+ validWordForDictionary = null;
+
+ // 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
+ final SettingsValues sv = Settings.getValues();
+ if (sv.mAddToPersonalDictionary // require the setting
+ && sv.mAutoCorrectEnabled == sv.mAutoCorrectionEnabledPerUserSettings // don't add if user wants autocorrect but input field does not, see https://github.com/Helium314/HeliBoard/issues/427#issuecomment-1905438000
+ && mDictionaryGroups.get(0).hasDict(Dictionary.TYPE_USER_HISTORY, mDictionaryGroups.get(0).mAccount) // require personalized suggestions
+ && !wasAutoCapitalized // we can't be 100% sure about what the user intended to type, so better don't add it
+ && words.length == 1) { // ignore if more than a single word, this only happens with (badly working) spaceAwareGesture
+ addToPersonalDictionaryIfInvalidButInHistory(suggestion, validWordForDictionary);
+ }
+
+ NgramContext ngramContextForCurrentWord = ngramContext;
+ for (int i = 0; i < words.length; i++) {
+ final String currentWord = words[i];
+ final boolean wasCurrentWordAutoCapitalized = (i == 0) && wasAutoCapitalized;
+ // add to history for preferred dictionary group, to avoid mixing languages in history
+ addWordToUserHistory(getCurrentlyPreferredDictionaryGroup(), ngramContextForCurrentWord, currentWord,
+ wasCurrentWordAutoCapitalized, (int) timeStampInSeconds,
+ blockPotentiallyOffensive);
+ ngramContextForCurrentWord =
+ ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord));
+
+ // remove manually entered blacklisted words from blacklist
+ for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
+ if (dictionaryGroup.blacklist.remove(currentWord))
+ removeWordFromBlacklistFile(currentWord, dictionaryGroup.blacklistFileName);
+ }
+ }
+ }
+
+ @Override public void adjustConfidences(final String word, final boolean wasAutoCapitalized) {
+ if (mDictionaryGroups.size() > 1 && !word.contains(Constants.WORD_SEPARATOR))
+ adjustConfidencesInternal(word, wasAutoCapitalized);
+ }
+
+ private boolean[] adjustConfidencesInternal(final String word, final boolean wasAutoCapitalized) {
+ final boolean[] 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 = StringUtilsKt.decapitalize(word, getCurrentLocale());
+ else
+ decapitalizedSuggestion = word;
+ for (int i = 0; i < mDictionaryGroups.size(); i ++) {
+ final DictionaryGroup dictionaryGroup = mDictionaryGroups.get(i);
+ final boolean isValidWord = isValidWord(word, ALL_DICTIONARY_TYPES, dictionaryGroup);
+ if (isValidWord || (wasAutoCapitalized && isValidWord(decapitalizedSuggestion, ALL_DICTIONARY_TYPES, dictionaryGroup)))
+ dictionaryGroup.increaseConfidence();
+ else dictionaryGroup.decreaseConfidence();
+ validWordForDictionary[i] = isValidWord;
+ }
+ return validWordForDictionary;
+ }
+
+ // main and secondary isValid provided to avoid duplicate lookups
+ private void addToPersonalDictionaryIfInvalidButInHistory(String word, boolean[] validWordForDictionary) {
+ final DictionaryGroup dictionaryGroup = getClearlyPreferredDictionaryGroupOrNull();
+ if (dictionaryGroup == null) return;
+ if (validWordForDictionary == null
+ ? isValidWord(word, ALL_DICTIONARY_TYPES, dictionaryGroup)
+ : validWordForDictionary[mDictionaryGroups.indexOf(dictionaryGroup)]
+ )
+ return;
+
+ final ExpandableBinaryDictionary userDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER);
+ final Dictionary userHistoryDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY);
+ if (userDict == null || userHistoryDict == null) return;
+
+ // user history always reports words as invalid, so here we need to check isInDictionary instead
+ // update: now getFrequency returns the correct value instead of -1, so better use that
+ // a little testing shows that after 2 times adding, the frequency is 111, and then rises slowly with usage
+ // 120 is after 3 uses of the word, so we simply require more than that.
+ // 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
+ if (userHistoryDict.getFrequency(word) > 120) {
+ if (userDict.isInDictionary(word)) // is this check necessary?
+ return;
+ ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(() ->
+ UserDictionary.Words.addWord(userDict.mContext, word,
+ 250 /*FREQUENCY_FOR_USER_DICTIONARY_ADDS*/, null, dictionaryGroup.mLocale));
+ }
+ }
+
+ private void putWordIntoValidSpellingWordCache(
+ @NonNull final String caller,
+ @NonNull final String originalWord) {
+ if (mValidSpellingWordWriteCache == null) {
+ return;
+ }
+
+ final String lowerCaseWord = originalWord.toLowerCase(getCurrentLocale());
+ final boolean lowerCaseValid = isValidSpellingWord(lowerCaseWord);
+ mValidSpellingWordWriteCache.put(lowerCaseWord, lowerCaseValid);
+
+ final String capitalWord =
+ StringUtils.capitalizeFirstAndDowncaseRest(originalWord, getCurrentLocale());
+ final boolean capitalValid;
+ if (lowerCaseValid) {
+ // The lower case form of the word is valid, so the upper case must be valid.
+ capitalValid = true;
+ } else {
+ capitalValid = isValidSpellingWord(capitalWord);
+ }
+ mValidSpellingWordWriteCache.put(capitalWord, capitalValid);
+ }
+
+ private void addWordToUserHistory(final DictionaryGroup dictionaryGroup,
+ final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized,
+ final int timeStampInSeconds, final boolean blockPotentiallyOffensive) {
+ final ExpandableBinaryDictionary userHistoryDictionary =
+ dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY);
+ if (userHistoryDictionary == null || !hasLocale(userHistoryDictionary.mLocale)) {
+ return;
+ }
+ final int mainFreq = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN, null)
+ ? dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(word)
+ : Dictionary.NOT_A_PROBABILITY;
+ if (mainFreq == 0 && blockPotentiallyOffensive) {
+ return;
+ }
+ if (mTryChangingWords)
+ mTryChangingWords = ngramContext.changeWordIfAfterBeginningOfSentence(mChangeFrom, mChangeTo);
+ final String secondWord;
+ // check for isBeginningOfSentenceContext too, because not all text fields auto-capitalize in this case
+ // and even if the user capitalizes manually, they most likely don't want the capitalized form suggested
+ if (wasAutoCapitalized || ngramContext.isBeginningOfSentenceContext()) {
+ // used word with lower-case first letter instead of all lower-case, as auto-capitalize
+ // does not affect the other letters
+ final String decapitalizedWord = StringUtilsKt.decapitalize(word, dictionaryGroup.mLocale);
+ 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
+ // wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version
+ // of that contact's name which would end up popping in suggestions.
+ secondWord = word;
+ } else {
+ // If however the word is not in the dictionary, or exists as a de-capitalized word
+ // only, then we consider that was a lower-case word that had been auto-capitalized.
+ secondWord = decapitalizedWord;
+ mTryChangingWords = true;
+ mChangeFrom = word;
+ mChangeTo = secondWord;
+ }
+ } 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)
+ ? dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord)
+ : Dictionary.NOT_A_PROBABILITY;
+ if (mainFreq < lowerCaseFreqInMainDict
+ && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) {
+ // Use lower cased word as the word can be a distracter of the popular word.
+ secondWord = lowerCasedWord;
+ } else {
+ secondWord = word;
+ }
+ }
+ // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
+ // We don't add words with 0-frequency (assuming they would be profanity etc.).
+ // comment: so this means words not in main dict are always invalid... weird (but still works)
+ final boolean isValid = mainFreq > 0;
+ UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, secondWord,
+ isValid, timeStampInSeconds);
+ }
+
+ /** returns the dictionaryGroup with most confidence, first group when tied */
+ private DictionaryGroup getCurrentlyPreferredDictionaryGroup() {
+ DictionaryGroup dictGroup = null;
+ int highestConfidence = -1;
+ for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
+ if (dictionaryGroup.mConfidence > highestConfidence) {
+ dictGroup = dictionaryGroup;
+ highestConfidence = dictGroup.mConfidence;
+ }
+ }
+ return dictGroup;
+ }
+
+ private DictionaryGroup getClearlyPreferredDictionaryGroupOrNull() {
+ // we want one clearly preferred group and return null otherwise
+ if (mDictionaryGroups.size() == 1)
+ return mDictionaryGroups.get(0);
+ // that preferred group should have at least MAX_CONFIDENCE, and all others should have 0 (we want to be really sure!)
+ int preferredGroup = -1;
+ for (int i = 0; i < mDictionaryGroups.size(); i ++) {
+ final DictionaryGroup dictionaryGroup = mDictionaryGroups.get(i);
+ if (dictionaryGroup.mConfidence == 0) continue;
+ if (dictionaryGroup.mConfidence >= DictionaryGroup.MAX_CONFIDENCE && preferredGroup == -1) {
+ preferredGroup = i;
+ continue;
+ }
+ // either we have 2 groups with high confidence, or a group with low but non-0 confidence
+ // in either case, we're not sure enough and return null
+ return null;
+ }
+ if (preferredGroup == -1) return null;
+ return mDictionaryGroups.get(preferredGroup);
+ }
+
+ private void removeWord(final String dictName, final String word) {
+ final ExpandableBinaryDictionary dictionary = getCurrentlyPreferredDictionaryGroup().getSubDict(dictName);
+ if (dictionary != null) {
+ dictionary.removeUnigramEntryDynamically(word);
+ }
+ }
+
+ @Override
+ public void unlearnFromUserHistory(final String word,
+ @NonNull final NgramContext ngramContext, final long timeStampInSeconds,
+ final int eventType) {
+ // TODO: Decide whether or not to remove the word on EVENT_BACKSPACE.
+ if (eventType != Constants.EVENT_BACKSPACE) {
+ removeWord(Dictionary.TYPE_USER_HISTORY, word);
+ }
+
+ // Update the spelling cache after unlearning. Words that are removed from user history
+ // and appear in no other language model are not considered valid.
+ putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.toLowerCase());
+ }
+
+ // TODO: Revise the way to fusion suggestion results.
+ @Override
+ @SuppressWarnings("unchecked")
+ @NonNull public SuggestionResults getSuggestionResults(ComposedData composedData,
+ NgramContext ngramContext, @NonNull final Keyboard keyboard,
+ SettingsValuesForSuggestion settingsValuesForSuggestion, int sessionId,
+ int inputStyle) {
+ long proximityInfoHandle = keyboard.getProximityInfo().getNativeProximityInfo();
+ final SuggestionResults suggestionResults = new SuggestionResults(
+ SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext(),
+ false /* firstSuggestionExceedsConfidenceThreshold */);
+ final float[] weightOfLangModelVsSpatialModel =
+ new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL };
+
+ // start getting suggestions for non-main locales first, but in background
+ final ArrayList[] otherDictionarySuggestions = (ArrayList[]) 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(() -> {
+ otherDictionarySuggestions[index] = getSuggestions(composedData,
+ ngramContext, settingsValuesForSuggestion, sessionId, proximityInfoHandle,
+ weightOfLangModelVsSpatialModel, dictionaryGroup);
+ waitForOtherDictionaries.countDown();
+ });
+ }
+ } else
+ waitForOtherDictionaries = null;
+
+ // get main locale suggestions
+ final ArrayList dictionarySuggestions = getSuggestions(composedData,
+ ngramContext, settingsValuesForSuggestion, sessionId, proximityInfoHandle,
+ weightOfLangModelVsSpatialModel, mDictionaryGroups.get(0));
+ suggestionResults.addAll(dictionarySuggestions);
+ if (null != suggestionResults.mRawSuggestions) {
+ suggestionResults.mRawSuggestions.addAll(dictionarySuggestions);
+ }
+
+ // 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);
+ }
+ for (int i = 1; i < mDictionaryGroups.size(); i ++) {
+ suggestionResults.addAll(otherDictionarySuggestions[i - 1]);
+ if (null != suggestionResults.mRawSuggestions) {
+ suggestionResults.mRawSuggestions.addAll(otherDictionarySuggestions[i - 1]);
+ }
+ }
+ }
+
+ return suggestionResults;
+ }
+
+ private ArrayList getSuggestions(ComposedData composedData,
+ NgramContext ngramContext, SettingsValuesForSuggestion settingsValuesForSuggestion,
+ int sessionId, long proximityInfoHandle, float[] weightOfLangModelVsSpatialModel,
+ DictionaryGroup dictGroup) {
+ final ArrayList suggestions = new ArrayList<>();
+ float weightForLocale = composedData.mIsBatchMode
+ ? dictGroup.getWeightForGesturingInLocale(mDictionaryGroups)
+ : dictGroup.getWeightForTypingInLocale(mDictionaryGroups);
+ for (final String dictType : ALL_DICTIONARY_TYPES) {
+ final Dictionary dictionary = dictGroup.getDict(dictType);
+ if (null == dictionary) continue;
+ final ArrayList dictionarySuggestions =
+ dictionary.getSuggestions(composedData, ngramContext,
+ proximityInfoHandle, settingsValuesForSuggestion, sessionId,
+ weightForLocale, weightOfLangModelVsSpatialModel);
+ if (null == dictionarySuggestions) continue;
+
+ // for some reason, garbage words are produced when glide typing
+ // for user history and main dictionary we can filter them out by checking whether the
+ // dictionary actually contains the word
+ // but personal dictionary and addon dictionaries may contain shortcuts, which do not
+ // pass an isInDictionary check (e.g. emojis)
+ // (if the main dict contains shortcuts to non-words, this will break)
+ final boolean checkForGarbage = composedData.mIsBatchMode && (dictType.equals(Dictionary.TYPE_USER_HISTORY) || dictType.equals(Dictionary.TYPE_MAIN));
+ for (SuggestedWordInfo info : dictionarySuggestions) {
+ final String word = info.getWord();
+ if (!isBlacklisted(word)) { // don't add blacklisted words
+ if (checkForGarbage
+ // only check history and "main main dictionary"
+ // consider the user might use custom main dictionary containing shortcuts
+ // assume this is unlikely to happen, and take care about common shortcuts that are not actual words (emoji, symbols)
+ && word.length() > 2 // should exclude most symbol shortcuts
+ && info.mSourceDict.mDictType.equals(dictType) // dictType is always main, but info.mSourceDict.mDictType contains the actual dict (main dict is a dictionary group)
+ && !StringUtils.mightBeEmoji(word) // emojis often have more than 2 chars; simplified check for performance reasons
+ && !dictionary.isInDictionary(word))
+ continue;
+ suggestions.add(info);
+ }
+ }
+ }
+ 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);
+ if (cachedValue != null) {
+ return cachedValue;
+ }
+ }
+ boolean result = false;
+ for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
+ if (isValidWord(word, ALL_DICTIONARY_TYPES, dictionaryGroup)) {
+ result = true;
+ break;
+ }
+ }
+ if (mValidSpellingWordReadCache != null)
+ mValidSpellingWordReadCache.put(word, result);
+ return result;
+ }
+
+ // 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, mDictionaryGroups.get(0));
+ }
+
+ private boolean isValidWord(final String word, final String[] dictionariesToCheck, final DictionaryGroup dictionaryGroup) {
+ if (TextUtils.isEmpty(word)) {
+ return false;
+ }
+ if (isBlacklisted(word)) return false;
+ for (final String dictType : dictionariesToCheck) {
+ final Dictionary dictionary = dictionaryGroup.getDict(dictType);
+ // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
+ // would be immutable once it's finished initializing, but concretely a null test is
+ // probably good enough for the time being.
+ if (null == dictionary) continue;
+ if (dictionary.isValidWord(word)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isBlacklisted(final String word) {
+ for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
+ if (dictionaryGroup.blacklist.contains(word))
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void removeWord(String word) {
+ for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
+ removeWordFromGroup(word, dictionaryGroup);
+ }
+ }
+
+ private void removeWordFromGroup(String word, DictionaryGroup group) {
+ // remove from user history
+ final ExpandableBinaryDictionary historyDict = group.getSubDict(Dictionary.TYPE_USER_HISTORY);
+ if (historyDict != null) {
+ historyDict.removeUnigramEntryDynamically(word);
+ }
+ // and from personal dictionary
+ final ExpandableBinaryDictionary userDict = group.getSubDict(Dictionary.TYPE_USER);
+ if (userDict != null) {
+ userDict.removeUnigramEntryDynamically(word);
+ }
+
+ final ExpandableBinaryDictionary contactsDict = group.getSubDict(Dictionary.TYPE_CONTACTS);
+ if (contactsDict != null) {
+ if (contactsDict.isInDictionary(word)) {
+ contactsDict.removeUnigramEntryDynamically(word); // will be gone until next reload of dict
+ addToBlacklist(word, group);
+ return;
+ }
+ }
+ if (!group.hasDict(Dictionary.TYPE_MAIN, null))
+ return;
+ if (group.getDict(Dictionary.TYPE_MAIN).isValidWord(word)) {
+ addToBlacklist(word, group);
+ return;
+ }
+ final String lowercase = word.toLowerCase(group.mLocale);
+ if (group.getDict(Dictionary.TYPE_MAIN).isValidWord(lowercase)) {
+ addToBlacklist(lowercase, group);
+ }
+ }
+
+ private void addToBlacklist(final String word, final DictionaryGroup group) {
+ if (!group.blacklist.add(word))
+ return;
+ ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(() -> {
+ try {
+ FileOutputStream fos = new FileOutputStream(group.blacklistFileName, true);
+ fos.write((word + "\n").getBytes(StandardCharsets.UTF_8));
+ fos.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Exception while trying to write blacklist", e);
+ }
+ });
+ }
+
+ private ArrayList readBlacklistFile(final String filename) {
+ final ArrayList blacklist = new ArrayList<>();
+ if (filename == null) return blacklist;
+ File blacklistFile = new File(filename);
+ if (!blacklistFile.exists()) return blacklist;
+ try {
+ final Scanner scanner = new Scanner(blacklistFile, StandardCharsets.UTF_8.name()).useDelimiter("\n");
+ while (scanner.hasNext()) {
+ blacklist.add(scanner.next());
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Exception while reading blacklist", e);
+ }
+ return blacklist;
+ }
+
+ private void removeWordFromBlacklistFile(String word, String filename) {
+ ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(() -> {
+ try {
+ ArrayList blacklist = readBlacklistFile(filename);
+ blacklist.remove(word);
+ FileOutputStream fos = new FileOutputStream(filename);
+ for (String entry : blacklist) {
+ fos.write((entry + "\n").getBytes(StandardCharsets.UTF_8));
+ }
+ fos.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Exception while trying to write blacklist" + filename, e);
+ }
+ });
+
+ }
+
+ @Override
+ public boolean clearUserHistoryDictionary(final Context context) {
+ for (DictionaryGroup dictionaryGroup : mDictionaryGroups) {
+ final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY);
+ if (dictionary == null) {
+ return false; // should only ever happen for primary dictionary, so this is safe
+ }
+ dictionary.clear();
+ }
+ return true;
+ }
+
+ @Override
+ public String localesAndConfidences() {
+ if (mDictionaryGroups.size() < 2) return null;
+ final StringBuilder sb = new StringBuilder();
+ for (final DictionaryGroup dictGroup : mDictionaryGroups) {
+ if (sb.length() > 0)
+ sb.append(", ");
+ sb.append(dictGroup.mLocale).append(" ").append(dictGroup.mConfidence);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public void dumpDictionaryForDebug(final String 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.");
+ return;
+ }
+ dictToDump.dumpAllWordsForDebug();
+ }
+
+ @Override
+ // this is unused, so leave it for now
+ @NonNull public List getDictionaryStats(final Context context) {
+ final ArrayList statsOfEnabledSubDicts = new ArrayList<>();
+ for (final String dictType : DYNAMIC_DICTIONARY_TYPES) {
+ final ExpandableBinaryDictionary dictionary = mDictionaryGroups.get(0).getSubDict(dictType);
+ if (dictionary == null) continue;
+ statsOfEnabledSubDicts.add(dictionary.getDictionaryStats());
+ }
+ return statsOfEnabledSubDicts;
+ }
+
+ @Override
+ public String dump(final Context context) {
+ return "";
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.kt b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.kt
deleted file mode 100644
index c64158b01..000000000
--- a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.kt
+++ /dev/null
@@ -1,825 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- * modified
- * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
- */
-package helium314.keyboard.latin
-
-import android.Manifest
-import android.content.Context
-import android.provider.UserDictionary
-import android.util.LruCache
-import helium314.keyboard.keyboard.Keyboard
-import helium314.keyboard.keyboard.emoji.SupportedEmojis
-import helium314.keyboard.latin.DictionaryFacilitator.DictionaryInitializationListener
-import helium314.keyboard.latin.NgramContext.WordInfo
-import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
-import helium314.keyboard.latin.common.ComposedData
-import helium314.keyboard.latin.common.Constants
-import helium314.keyboard.latin.common.StringUtils
-import helium314.keyboard.latin.common.decapitalize
-import helium314.keyboard.latin.common.splitOnWhitespace
-import helium314.keyboard.latin.permissions.PermissionsUtil
-import helium314.keyboard.latin.personalization.UserHistoryDictionary
-import helium314.keyboard.latin.settings.Settings
-import helium314.keyboard.latin.settings.SettingsValuesForSuggestion
-import helium314.keyboard.latin.utils.Log
-import helium314.keyboard.latin.utils.SubtypeSettings
-import helium314.keyboard.latin.utils.SuggestionResults
-import helium314.keyboard.latin.utils.getSecondaryLocales
-import helium314.keyboard.latin.utils.locale
-import helium314.keyboard.latin.utils.prefs
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import java.io.File
-import java.io.IOException
-import java.util.Locale
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-
-/**
- * Facilitates interaction with different kinds of dictionaries. Provides APIs
- * to instantiate and select the correct dictionaries (based on language and settings),
- * update entries and fetch suggestions.
- *
- *
- * Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as
- * a client for interacting with dictionaries.
- */
-class DictionaryFacilitatorImpl : DictionaryFacilitator {
- private var dictionaryGroups = listOf(DictionaryGroup())
-
- @Volatile
- private var mLatchForWaitingLoadingMainDictionaries = CountDownLatch(0)
-
- // The library does not deal well with ngram history for auto-capitalized words, so we adjust
- // the ngram context to store next word suggestions for such cases.
- // todo: this is awful, find a better solution / workaround
- // or remove completely? not sure if it's actually an improvement
- // should be fixed in the library, but that's not feasible with current user-provides-library approach
- // added in 12cbd43bda7d0f0cd73925e9cf836de751c32ed0 / https://github.com/Helium314/HeliBoard/issues/135
- private var tryChangingWords = false
- private var changeFrom = ""
- private var changeTo = ""
-
- // todo: write cache never set, and never read (only written)
- // tried to use read cache for a while, but small performance improvements are not worth the work,
- // see https://github.com/Helium314/HeliBoard/issues/307
- private var mValidSpellingWordReadCache: LruCache? = null
- private var mValidSpellingWordWriteCache: LruCache? = null
-
- private val scope = CoroutineScope(Dispatchers.Default)
-
- override fun setValidSpellingWordReadCache(cache: LruCache) {
- mValidSpellingWordReadCache = cache
- }
-
- override fun setValidSpellingWordWriteCache(cache: LruCache) {
- mValidSpellingWordWriteCache = cache
- }
-
- // judging by usage before adding multilingual typing, this should check primary group locale only
- override fun isForLocale(locale: Locale?): Boolean {
- return locale != null && locale == dictionaryGroups[0].locale
- }
-
- override fun onStartInput() {
- }
-
- override fun onFinishInput(context: Context) {
- for (dictGroup in dictionaryGroups) {
- DictionaryFacilitator.ALL_DICTIONARY_TYPES.forEach { dictGroup.getDict(it)?.onFinishInput() }
- }
- }
-
- override fun isActive(): Boolean {
- return dictionaryGroups[0].locale.language.isNotEmpty()
- }
-
- override fun getMainLocale(): Locale {
- return dictionaryGroups[0].locale
- }
-
- override fun getCurrentLocale(): Locale {
- return currentlyPreferredDictionaryGroup.locale
- }
-
- override fun usesSameSettings(locales: List, contacts: Boolean, apps: Boolean, personalization: Boolean): Boolean {
- val dictGroup = dictionaryGroups[0] // settings are the same for all groups
- return contacts == dictGroup.hasDict(Dictionary.TYPE_CONTACTS)
- && apps == dictGroup.hasDict(Dictionary.TYPE_APPS)
- && personalization == dictGroup.hasDict(Dictionary.TYPE_USER_HISTORY)
- && locales.size == dictionaryGroups.size
- && locales.none { findDictionaryGroupWithLocale(dictionaryGroups, it) == null }
- }
-
- // -------------- managing (loading & closing) dictionaries ------------
-
- override fun resetDictionaries(
- context: Context,
- newLocale: Locale,
- useContactsDict: Boolean,
- useAppsDict: Boolean,
- usePersonalizedDicts: Boolean,
- forceReloadMainDictionary: Boolean,
- dictNamePrefix: String,
- listener: DictionaryInitializationListener?
- ) {
- Log.i(TAG, "resetDictionaries, force reloading main dictionary: $forceReloadMainDictionary")
-
- val locales = getUsedLocales(newLocale, context)
-
- val subDictTypesToUse = listOfNotNull(
- Dictionary.TYPE_USER,
- if (useAppsDict) Dictionary.TYPE_APPS else null,
- if (usePersonalizedDicts) Dictionary.TYPE_USER_HISTORY else null,
- if (useContactsDict && PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS))
- Dictionary.TYPE_CONTACTS else null
- )
-
- val (newDictionaryGroups, existingDictsToCleanup) =
- getNewDictGroupsAndDictsToCleanup(locales, subDictTypesToUse, forceReloadMainDictionary, dictNamePrefix, context)
-
- // Replace Dictionaries.
- val oldDictionaryGroups: List
- synchronized(this) {
- oldDictionaryGroups = dictionaryGroups
- dictionaryGroups = newDictionaryGroups
- if (hasAtLeastOneUninitializedMainDictionary()) {
- asyncReloadUninitializedMainDictionaries(context, locales, listener)
- }
- }
-
- listener?.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary())
-
- // Clean up old dictionaries.
- existingDictsToCleanup.forEach { (locale, dictTypes) ->
- val dictGroupToCleanup = findDictionaryGroupWithLocale(oldDictionaryGroups, locale) ?: return@forEach
- for (dictType in dictTypes) {
- dictGroupToCleanup.closeDict(dictType)
- }
- }
-
- mValidSpellingWordWriteCache?.evictAll()
- mValidSpellingWordReadCache?.evictAll()
- }
-
- /** creates dictionaryGroups for [newLocales] with given [newSubDictTypes], trying to re-use existing dictionaries.
- * returns the new dictionaryGroups and unused dictionary types by locale */
- private fun getNewDictGroupsAndDictsToCleanup(
- newLocales: Collection,
- newSubDictTypes: Collection,
- forceReload: Boolean,
- dictNamePrefix: String,
- context: Context
- ): Pair, Map>> {
- // Gather all dictionaries by locale. We may remove some from the list later.
- val existingDictsToCleanup = HashMap>()
- for (dictGroup in dictionaryGroups) {
- existingDictsToCleanup[dictGroup.locale] = DictionaryFacilitator.ALL_DICTIONARY_TYPES
- .filterTo(mutableListOf()) { dictGroup.hasDict(it) }
- }
-
- // create new dictionary groups and remove dictionaries to re-use from existingDictsToCleanup
- val newDictionaryGroups = mutableListOf()
- for (locale in newLocales) {
- // get existing dictionary group for new locale
- val oldDictGroupForLocale = findDictionaryGroupWithLocale(dictionaryGroups, locale)
- val dictTypesToCleanupForLocale = existingDictsToCleanup[locale]
-
- // create new or re-use already loaded main dict
- val mainDict: Dictionary?
- if (forceReload || oldDictGroupForLocale == null
- || !oldDictGroupForLocale.hasDict(Dictionary.TYPE_MAIN)
- ) {
- mainDict = null // null main dicts will be loaded later in asyncReloadUninitializedMainDictionaries
- } else {
- mainDict = oldDictGroupForLocale.getDict(Dictionary.TYPE_MAIN)
- dictTypesToCleanupForLocale?.remove(Dictionary.TYPE_MAIN)
- }
-
- // create new or re-use already loaded sub-dicts
- val subDicts: MutableMap = HashMap()
- for (subDictType in newSubDictTypes) {
- val subDict: ExpandableBinaryDictionary
- if (forceReload || oldDictGroupForLocale == null
- || !oldDictGroupForLocale.hasDict(subDictType)
- ) {
- // Create a new dictionary.
- subDict = createSubDict(subDictType, context, locale, null, dictNamePrefix) ?: continue
- } else {
- // Reuse the existing dictionary.
- subDict = oldDictGroupForLocale.getSubDict(subDictType) ?: continue
- dictTypesToCleanupForLocale?.remove(subDictType)
- }
- subDicts[subDictType] = subDict
- }
- val newDictGroup = DictionaryGroup(locale, mainDict, subDicts, context)
- newDictionaryGroups.add(newDictGroup)
- }
- return newDictionaryGroups to existingDictsToCleanup
- }
-
- private fun asyncReloadUninitializedMainDictionaries(
- context: Context, locales: Collection, listener: DictionaryInitializationListener?
- ) {
- val latchForWaitingLoadingMainDictionary = CountDownLatch(1)
- mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary
- scope.launch {
- try {
- val dictGroupsWithNewMainDict = locales.mapNotNull {
- val dictionaryGroup = findDictionaryGroupWithLocale(dictionaryGroups, it)
- if (dictionaryGroup == null) {
- Log.w(TAG, "Expected a dictionary group for $it but none found")
- return@mapNotNull null // This should never happen
- }
- if (dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.isInitialized == true) null
- else dictionaryGroup to DictionaryFactory.createMainDictionaryCollection(context, it)
- }
- synchronized(this) {
- dictGroupsWithNewMainDict.forEach { (dictGroup, mainDict) ->
- dictGroup.setMainDict(mainDict)
- }
- }
-
- listener?.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary())
- latchForWaitingLoadingMainDictionary.countDown()
- } catch (e: Throwable) {
- Log.e(TAG, "could not initialize main dictionaries for $locales", e)
- }
- }
- }
-
- override fun closeDictionaries() {
- val dictionaryGroupsToClose: List
- synchronized(this) {
- dictionaryGroupsToClose = dictionaryGroups
- dictionaryGroups = listOf(DictionaryGroup())
- }
- for (dictionaryGroup in dictionaryGroupsToClose) {
- for (dictType in DictionaryFacilitator.ALL_DICTIONARY_TYPES) {
- dictionaryGroup.closeDict(dictType)
- }
- }
- }
-
- // The main dictionaries are loaded asynchronously. Don't cache the return value of these methods.
- override fun hasAtLeastOneInitializedMainDictionary(): Boolean =
- dictionaryGroups.any { it.getDict(Dictionary.TYPE_MAIN)?.isInitialized == true }
-
- override fun hasAtLeastOneUninitializedMainDictionary(): Boolean =
- dictionaryGroups.any { it.getDict(Dictionary.TYPE_MAIN)?.isInitialized != true }
-
- @Throws(InterruptedException::class)
- override fun waitForLoadingMainDictionaries(timeout: Long, unit: TimeUnit) {
- mLatchForWaitingLoadingMainDictionaries.await(timeout, unit)
- }
-
- // -------------- actual dictionary stuff like getting suggestions ------------
-
- override fun addToUserHistory(
- suggestion: String, wasAutoCapitalized: Boolean, ngramContext: NgramContext,
- timeStampInSeconds: Long, blockPotentiallyOffensive: Boolean
- ) {
- // Update the spelling cache before learning. Words that are not yet added to user history
- // and appear in no other language model are not considered valid.
- putWordIntoValidSpellingWordCache("addToUserHistory", suggestion)
-
- val words = suggestion.splitOnWhitespace().dropLastWhile { it.isEmpty() }
-
- // increase / decrease confidence
- if (words.size == 1) // ignore if more than a single word, which only happens with (badly working) spaceAwareGesture
- adjustConfidences(suggestion, wasAutoCapitalized)
-
- // Add word to user dictionary if it is in no other dictionary except user history dictionary (i.e. typed again).
- val sv = Settings.getValues()
- if (sv.mAddToPersonalDictionary // require the opt-in
- && sv.mAutoCorrectEnabled == sv.mAutoCorrectionEnabledPerUserSettings // don't add if user wants autocorrect but input field does not, see https://github.com/Helium314/HeliBoard/issues/427#issuecomment-1905438000
- && dictionaryGroups[0].hasDict(Dictionary.TYPE_USER_HISTORY) // require personalized suggestions
- && !wasAutoCapitalized // we can't be 100% sure about what the user intended to type, so better don't add it
- && words.size == 1 // only single words
- ) {
- addToPersonalDictionaryIfInvalidButInHistory(suggestion)
- }
-
- var ngramContextForCurrentWord = ngramContext
- val preferredGroup = currentlyPreferredDictionaryGroup
- for (i in words.indices) {
- val currentWord = words[i]
- val wasCurrentWordAutoCapitalized = (i == 0) && wasAutoCapitalized
- // add to history for preferred dictionary group, to avoid mixing languages in history
- addWordToUserHistory(
- preferredGroup, ngramContextForCurrentWord, currentWord,
- wasCurrentWordAutoCapitalized, timeStampInSeconds.toInt(), blockPotentiallyOffensive
- )
- ngramContextForCurrentWord = ngramContextForCurrentWord.getNextNgramContext(WordInfo(currentWord))
-
- // remove manually entered blacklisted words from blacklist for likely matching languages
- dictionaryGroups.filter { it.confidence == preferredGroup.confidence }.forEach {
- it.removeFromBlacklist(currentWord)
- }
- }
- }
-
- private fun addWordToUserHistory(
- dictionaryGroup: DictionaryGroup, ngramContext: NgramContext, word: String, wasAutoCapitalized: Boolean,
- timeStampInSeconds: Int, blockPotentiallyOffensive: Boolean
- ) {
- val userHistoryDictionary = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY) ?: return
-
- val mainFreq = dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.getFrequency(word) ?: Dictionary.NOT_A_PROBABILITY
- if (mainFreq == 0 && blockPotentiallyOffensive)
- return
- if (tryChangingWords)
- tryChangingWords = ngramContext.changeWordIfAfterBeginningOfSentence(changeFrom, changeTo)
-
- val wordToUse: String
- // Check for isBeginningOfSentenceContext too, because not all text fields auto-capitalize in this case.
- // Even if the user capitalizes manually, they most likely don't want the capitalized form suggested.
- if (wasAutoCapitalized || ngramContext.isBeginningOfSentenceContext) {
- val decapitalizedWord = word.decapitalize(dictionaryGroup.locale) // try undoing auto-capitalization
- if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroup)
- && !isValidWord(decapitalizedWord, DictionaryFacilitator.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
- // wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version
- // of that contact's name which would end up popping in suggestions.
- wordToUse = word
- } else {
- // 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.
- wordToUse = decapitalizedWord
- tryChangingWords = true
- changeFrom = word
- changeTo = wordToUse
- }
- } 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.
- val lowerCasedWord = word.lowercase(dictionaryGroup.locale)
- val lowerCaseFreqInMainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.getFrequency(lowerCasedWord)
- ?: Dictionary.NOT_A_PROBABILITY
- wordToUse = if (mainFreq < lowerCaseFreqInMainDict
- && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT
- ) {
- // Use lower cased word as the word can be a distracter of the popular word.
- lowerCasedWord
- } else {
- word
- }
- }
- // We demote unrecognized words (frequency <= 0) by specifying them as "invalid".
- // We don't add words with 0-frequency (assuming they would be profanity etc.).
- val isValid = mainFreq > 0
- UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, wordToUse, isValid, timeStampInSeconds)
- }
-
- private fun addToPersonalDictionaryIfInvalidButInHistory(word: String) {
- if (word.length <= 1) return
- val dictionaryGroup = clearlyPreferredDictionaryGroup ?: return
- val userDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER) ?: return
- val userHistoryDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY) ?: return
- if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroup))
- return // valid word, no reason to auto-add it to personal dict
- if (userDict.isInDictionary(word))
- return // should never happen, but better be safe
-
- // User history always reports words as invalid, so we check the frequency instead.
- // Testing shows that after 2 times adding, the frequency is 111, and then rises slowly with usage (values vary slightly).
- // 120 is after 3 uses of the word, so we simply require more than that. todo: Could be made configurable.
- // Words added to dictionaries (user and history) seem to be found only after some delay.
- // This is not too bad, but it delays adding in case a user wants to fill a dictionary using this functionality
- if (userHistoryDict.getFrequency(word) > 120) {
- scope.launch {
- // adding can throw IllegalArgumentException: Unknown URL content://user_dictionary/words
- // https://stackoverflow.com/q/41474623 https://github.com/AnySoftKeyboard/AnySoftKeyboard/issues/490
- // apparently some devices don't have a dictionary? or it's just sporadic hiccups?
- runCatching { UserDictionary.Words.addWord(userDict.mContext, word, 250, null, dictionaryGroup.locale) }
- }
- }
- }
-
- private fun putWordIntoValidSpellingWordCache(caller: String, originalWord: String) {
- if (mValidSpellingWordWriteCache == null)
- return
-
- val lowerCaseWord = originalWord.lowercase(currentLocale)
- val lowerCaseValid = isValidSpellingWord(lowerCaseWord)
- mValidSpellingWordWriteCache?.put(lowerCaseWord, lowerCaseValid)
-
- val capitalWord = StringUtils.capitalizeFirstAndDowncaseRest(originalWord, currentLocale)
- val capitalValid = if (lowerCaseValid) {
- true // The lower case form of the word is valid, so the upper case must be valid.
- } else {
- isValidSpellingWord(capitalWord)
- }
- mValidSpellingWordWriteCache?.put(capitalWord, capitalValid)
- }
-
- override fun adjustConfidences(word: String, wasAutoCapitalized: Boolean) {
- if (dictionaryGroups.size == 1 || word.contains(Constants.WORD_SEPARATOR))
- return
-
- // if suggestion was auto-capitalized, check against both the suggestion and the de-capitalized suggestion
- val decapitalizedSuggestion = if (wasAutoCapitalized) word.decapitalize(currentLocale) else word
- dictionaryGroups.forEach {
- if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it)) {
- it.increaseConfidence()
- return@forEach
- }
- // also increase confidence if suggestion was auto-capitalized and the lowercase variant it valid
- if (wasAutoCapitalized && isValidWord(decapitalizedSuggestion, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it))
- it.increaseConfidence()
- else it.decreaseConfidence()
- }
- }
-
- /** the dictionaryGroup with most confidence, first group when tied */
- private val currentlyPreferredDictionaryGroup: DictionaryGroup get() = dictionaryGroups.maxBy { it.confidence }
-
- /** the only dictionary group, or the dictionaryGroup confidence >= DictionaryGroup.MAX_CONFIDENCE if all others have 0 */
- private val clearlyPreferredDictionaryGroup: DictionaryGroup? get() {
- if (dictionaryGroups.size == 1) return dictionaryGroups.first() // confidence not used if we only have a single group
-
- val preferred = currentlyPreferredDictionaryGroup
- if (preferred.confidence < DictionaryGroup.MAX_CONFIDENCE) return null
- if (dictionaryGroups.any { it.confidence > 0 && it !== preferred })
- return null
- return preferred
- }
-
- override fun unlearnFromUserHistory(word: String, ngramContext: NgramContext, timeStampInSeconds: Long, eventType: Int) {
- // TODO: Decide whether or not to remove the word on EVENT_BACKSPACE.
- if (eventType != Constants.EVENT_BACKSPACE) {
- currentlyPreferredDictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY)?.removeUnigramEntryDynamically(word)
- }
-
- // Update the spelling cache after unlearning. Words that are removed from user history
- // and appear in no other language model are not considered valid.
- putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.lowercase(Locale.getDefault()))
- }
-
- // TODO: Revise the way to fusion suggestion results.
- override fun getSuggestionResults(
- composedData: ComposedData, ngramContext: NgramContext, keyboard: Keyboard,
- settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int, inputStyle: Int
- ): SuggestionResults {
- val proximityInfoHandle = keyboard.proximityInfo.nativeProximityInfo
- val weightOfLangModelVsSpatialModel = floatArrayOf(Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL)
-
- val waitForOtherDicts = if (dictionaryGroups.size == 1) null else CountDownLatch(dictionaryGroups.size - 1)
- val suggestionsArray = Array?>(dictionaryGroups.size) { null }
- for (i in 1..dictionaryGroups.lastIndex) {
- scope.launch {
- suggestionsArray[i] = getSuggestions(composedData, ngramContext, settingsValuesForSuggestion, sessionId,
- proximityInfoHandle, weightOfLangModelVsSpatialModel, dictionaryGroups[i])
- waitForOtherDicts?.countDown()
- }
- }
- suggestionsArray[0] = getSuggestions(composedData, ngramContext, settingsValuesForSuggestion, sessionId,
- proximityInfoHandle, weightOfLangModelVsSpatialModel, dictionaryGroups[0])
- val suggestionResults = SuggestionResults(
- SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext, false
- )
- waitForOtherDicts?.await()
-
- suggestionsArray.forEach {
- if (it == null) return@forEach
- suggestionResults.addAll(it)
- suggestionResults.mRawSuggestions?.addAll(it)
- }
-
- return suggestionResults
- }
-
- private fun getSuggestions(
- composedData: ComposedData, ngramContext: NgramContext,
- settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int,
- proximityInfoHandle: Long, weightOfLangModelVsSpatialModel: FloatArray, dictGroup: DictionaryGroup
- ): List {
- val suggestions = ArrayList()
- val weightForLocale = dictGroup.getWeightForLocale(dictionaryGroups, composedData.mIsBatchMode)
- for (dictType in DictionaryFacilitator.ALL_DICTIONARY_TYPES) {
- val dictionary = dictGroup.getDict(dictType) ?: continue
- val dictionarySuggestions = dictionary.getSuggestions(composedData, ngramContext, proximityInfoHandle,
- settingsValuesForSuggestion, sessionId, weightForLocale, weightOfLangModelVsSpatialModel
- ) ?: continue
-
- // For some reason "garbage" words are produced when glide typing. For user history
- // and main dictionaries we can filter them out by checking whether the dictionary
- // actually contains the word. But personal and addon dictionaries may contain shortcuts,
- // which do not pass an isInDictionary check (e.g. emojis).
- // (if the main dict contains shortcuts to non-words, this will break!)
- val checkForGarbage = composedData.mIsBatchMode && (dictType == Dictionary.TYPE_USER_HISTORY || dictType == Dictionary.TYPE_MAIN)
-
- for (info in dictionarySuggestions) {
- val word = info.word
- if (isBlacklisted(word) || SupportedEmojis.isUnsupported(word)) // don't add blacklisted words and unsupported emojis
- continue
- if (checkForGarbage
- // consider the user might use custom main dictionary containing shortcuts
- // assume this is unlikely to happen, and take care about common shortcuts that are not actual words (emoji, symbols)
- && word.length > 2 // should exclude most symbol shortcuts
- && info.mSourceDict.mDictType == dictType // dictType is always main, but info.mSourceDict.mDictType contains the actual dict (main dict is a dictionary group)
- && !StringUtils.mightBeEmoji(word) // simplified check for performance reasons
- && !dictionary.isInDictionary(word)
- )
- continue
-
- if (word.length == 1 && info.mSourceDict.mDictType == "emoji" && !StringUtils.mightBeEmoji(word[0].code))
- continue
-
- suggestions.add(info)
- }
- }
- 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
- override fun isValidSpellingWord(word: String): Boolean {
- mValidSpellingWordReadCache?.get(word)?.let { return it }
- val result = dictionaryGroups.any { isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it) }
- mValidSpellingWordReadCache?.put(word, result)
- return result
- }
-
- // this is unused, so leave it for now (redirecting to isValidWord seems to defeat the purpose...)
- override fun isValidSuggestionWord(word: String): Boolean {
- return isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroups[0])
- }
-
- // todo: move into dictionaryGroup?
- private fun isValidWord(word: String, dictionariesToCheck: Array, dictionaryGroup: DictionaryGroup): Boolean {
- if (word.isEmpty() || dictionaryGroup.isBlacklisted(word)) return false
- return dictionariesToCheck.any { dictionaryGroup.getDict(it)?.isValidWord(word) == true }
- }
-
- private fun isBlacklisted(word: String): Boolean = dictionaryGroups.any { it.isBlacklisted(word) }
-
- override fun removeWord(word: String) {
- for (dictionaryGroup in dictionaryGroups) {
- dictionaryGroup.removeWord(word)
- }
- }
-
- override fun clearUserHistoryDictionary(context: Context) {
- for (dictionaryGroup in dictionaryGroups) {
- dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY)?.clear()
- }
- }
-
- override fun localesAndConfidences(): String? {
- if (dictionaryGroups.size < 2) return null
- return dictionaryGroups.joinToString(", ") { "${it.locale} ${it.confidence}" }
- }
-
- override fun dumpDictionaryForDebug(dictName: String) {
- val dictToDump = dictionaryGroups[0].getSubDict(dictName)
- if (dictToDump == null) {
- Log.e(TAG, ("Cannot dump $dictName. The dictionary is not being used for suggestion or cannot be dumped."))
- return
- }
- dictToDump.dumpAllWordsForDebug()
- }
-
- override fun getDictionaryStats(context: Context): List =
- DictionaryFacilitator.DYNAMIC_DICTIONARY_TYPES.flatMap { dictType ->
- dictionaryGroups.mapNotNull { it.getSubDict(dictType)?.dictionaryStats }
- }
-
- override fun dump(context: Context) = getDictionaryStats(context).joinToString("\n")
-
- companion object {
- private val TAG = DictionaryFacilitatorImpl::class.java.simpleName
-
- // HACK: This threshold is being used when adding a capitalized entry in the User History dictionary.
- private const val CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140
-
- private fun createSubDict(
- dictType: String, context: Context, locale: Locale, dictFile: File?, dictNamePrefix: String
- ): ExpandableBinaryDictionary? {
- try {
- return when (dictType) {
- Dictionary.TYPE_USER_HISTORY -> UserHistoryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
- Dictionary.TYPE_USER -> UserBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
- Dictionary.TYPE_CONTACTS -> ContactsBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
- Dictionary.TYPE_APPS -> AppsBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
- else -> throw IllegalArgumentException("unknown dictionary type $dictType")
- }
- } catch (e: SecurityException) {
- Log.e(TAG, "Cannot create dictionary: $dictType", e)
- } catch (e: IllegalArgumentException) {
- Log.e(TAG, "Cannot create dictionary: $dictType", e)
- }
- return null
- }
-
- private fun findDictionaryGroupWithLocale(dictGroups: List?, locale: Locale): DictionaryGroup? {
- return dictGroups?.firstOrNull { it.locale == locale }
- }
-
- private fun getUsedLocales(mainLocale: Locale, context: Context): Collection {
- val locales = hashSetOf(mainLocale)
- // adding secondary locales is a bit tricky since they depend on the subtype
- // but usually this is called with the selected subtype locale
- val selectedSubtype = SubtypeSettings.getSelectedSubtype(context.prefs())
- if (selectedSubtype.locale() == mainLocale) {
- locales.addAll(getSecondaryLocales(selectedSubtype.extraValue))
- } else {
- // probably we're called from the spell checker when using a different app as keyboard
- // so best bet is adding all secondary locales for matching main locale
- SubtypeSettings.getEnabledSubtypes(false).forEach {
- if (it.locale() == mainLocale)
- locales.addAll(getSecondaryLocales(it.extraValue))
- }
- }
- return locales
- }
- }
-}
-
-/** A group of dictionaries that work together for a single language. */
-private class DictionaryGroup(
- val locale: Locale = Locale(""),
- private var mainDict: Dictionary? = null,
- subDicts: Map = emptyMap(),
- context: Context? = null
-) {
- private val subDicts: ConcurrentHashMap = ConcurrentHashMap(subDicts)
-
- /** Removes a word from all dictionaries in this group. If the word is in a read-only dictionary, it is blacklisted. */
- fun removeWord(word: String) {
- // remove from user history
- getSubDict(Dictionary.TYPE_USER_HISTORY)?.removeUnigramEntryDynamically(word)
-
- // and from personal dictionary
- getSubDict(Dictionary.TYPE_USER)?.removeUnigramEntryDynamically(word)
-
- val contactsDict = getSubDict(Dictionary.TYPE_CONTACTS)
- if (contactsDict != null && contactsDict.isInDictionary(word)) {
- contactsDict.removeUnigramEntryDynamically(word) // will be gone until next reload of dict
- addToBlacklist(word)
- return
- }
-
- val appsDict = getSubDict(Dictionary.TYPE_APPS)
- if (appsDict != null && appsDict.isInDictionary(word)) {
- appsDict.removeUnigramEntryDynamically(word) // will be gone until next reload of dict
- addToBlacklist(word)
- return
- }
-
- val mainDict = mainDict ?: return
- if (mainDict.isValidWord(word)) {
- addToBlacklist(word)
- return
- }
-
- val lowercase = word.lowercase(locale)
- if (getDict(Dictionary.TYPE_MAIN)!!.isValidWord(lowercase)) {
- addToBlacklist(lowercase)
- }
- }
-
- // --------------- Confidence for multilingual typing -------------------
-
- // 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, with an exception when typing a single word not contained
- // in this language.
- var confidence = 1
-
- // allow to go above max confidence, for better determination of currently preferred language
- // when decreasing confidence or getting weight factor, limit to maximum
- fun increaseConfidence() {
- confidence += 1
- }
-
- // 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
- fun decreaseConfidence() {
- if (confidence > MAX_CONFIDENCE) confidence = MAX_CONFIDENCE
- else if (confidence > 0) {
- confidence -= 1
- }
- }
-
- fun getWeightForLocale(groups: List, isGesturing: Boolean) =
- getWeightForLocale(groups, if (isGesturing) 0.05f else 0.15f)
-
- // might need some more tuning
- fun getWeightForLocale(groups: List, step: Float): Float {
- if (groups.size == 1) return 1f
- if (confidence < 2) return 1f - step * (MAX_CONFIDENCE - confidence)
- for (group in groups) {
- if (group !== this && group.confidence >= confidence) return 1f - step / 2f
- }
- return 1f
- }
-
- // --------------- Blacklist -------------------
-
- private val scope = CoroutineScope(Dispatchers.IO)
-
- // words cannot be (permanently) removed from some dictionaries, so we use a blacklist for "removing" words
- private val blacklistFile = if (context?.filesDir == null) null
- else {
- val file = File(context.filesDir.absolutePath + File.separator + "blacklists" + File.separator + locale.toLanguageTag() + ".txt")
- if (file.isDirectory) file.delete() // this apparently was an issue in some versions
- if (file.parentFile?.exists() == true || file.parentFile?.mkdirs() == true) file
- else null
- }
-
- private val blacklist = hashSetOf().apply {
- if (blacklistFile?.isFile != true) return@apply
- scope.launch {
- synchronized(this) {
- try {
- addAll(blacklistFile.readLines())
- } catch (e: IOException) {
- Log.e(TAG, "Exception while trying to read blacklist from ${blacklistFile.name}", e)
- }
- }
- }
- }
-
- fun isBlacklisted(word: String) = blacklist.contains(word)
-
- fun addToBlacklist(word: String) {
- if (!blacklist.add(word) || blacklistFile == null) return
- scope.launch {
- synchronized(this) {
- try {
- if (blacklistFile.isDirectory) blacklistFile.delete()
- blacklistFile.appendText("$word\n")
- } catch (e: IOException) {
- Log.e(TAG, "Exception while trying to add word \"$word\" to blacklist ${blacklistFile.name}", e)
- }
- }
- }
- }
-
- fun removeFromBlacklist(word: String) {
- if (!blacklist.remove(word) || blacklistFile == null) return
- scope.launch {
- synchronized(this) {
- try {
- val newLines = blacklistFile.readLines().filterNot { it == word }
- blacklistFile.writeText(newLines.joinToString("\n"))
- } catch (e: IOException) {
- Log.e(TAG, "Exception while trying to remove word \"$word\" to blacklist ${blacklistFile.name}", e)
- }
- }
- }
- }
-
- // --------------- Dictionary handling -------------------
-
- fun setMainDict(newMainDict: Dictionary?) {
- // Close old dictionary if exists. Main dictionary can be assigned multiple times.
- val oldDict = mainDict
- mainDict = newMainDict
- if (oldDict != null && newMainDict !== oldDict)
- oldDict.close()
- }
-
- fun getDict(dictType: String): Dictionary? {
- if (dictType == Dictionary.TYPE_MAIN) {
- return mainDict
- }
- return getSubDict(dictType)
- }
-
- fun getSubDict(dictType: String): ExpandableBinaryDictionary? {
- return subDicts[dictType]
- }
-
- fun hasDict(dictType: String): Boolean {
- if (dictType == Dictionary.TYPE_MAIN) {
- return mainDict != null
- }
- return subDicts.containsKey(dictType)
- }
-
- fun closeDict(dictType: String) {
- val dict = if (Dictionary.TYPE_MAIN == dictType) {
- mainDict
- } else {
- subDicts.remove(dictType)
- }
- dict?.close()
- }
-
- companion object {
- private val TAG = DictionaryGroup::class.java.simpleName
- const val MAX_CONFIDENCE = 2
- }
-}
diff --git a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorLruCache.java b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorLruCache.java
index 2e98d0064..08096413c 100644
--- a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorLruCache.java
+++ b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorLruCache.java
@@ -26,7 +26,6 @@ public class DictionaryFacilitatorLruCache {
private final Object mLock = new Object();
private final DictionaryFacilitator mDictionaryFacilitator;
private boolean mUseContactsDictionary;
- private boolean mUseAppsDictionary;
private Locale mLocale;
public DictionaryFacilitatorLruCache(final Context context, final String dictionaryNamePrefix) {
@@ -59,8 +58,10 @@ public class DictionaryFacilitatorLruCache {
// Nothing to do if the locale is null. This would be the case before any get() calls.
if (mLocale != null) {
// Note: Given that personalized dictionaries are not used here; we can pass null account.
- mDictionaryFacilitator.resetDictionaries(mContext, mLocale, mUseContactsDictionary,
- mUseAppsDictionary, false, false, mDictionaryNamePrefix, null);
+ mDictionaryFacilitator.resetDictionaries(mContext, mLocale,
+ mUseContactsDictionary, false /* usePersonalizedDicts */,
+ false /* forceReloadMainDictionary */, null /* account */,
+ mDictionaryNamePrefix, null /* listener */);
}
}
@@ -76,18 +77,6 @@ public class DictionaryFacilitatorLruCache {
}
}
- public void setUseAppsDictionary(final boolean useAppsDictionary) {
- synchronized (mLock) {
- if (mUseAppsDictionary == useAppsDictionary) {
- // The value has not been changed.
- return;
- }
- mUseAppsDictionary = useAppsDictionary;
- resetDictionariesForLocaleLocked();
- waitForLoadingMainDictionary(mDictionaryFacilitator);
- }
- }
-
public DictionaryFacilitator get(final Locale locale) {
synchronized (mLock) {
if (!mDictionaryFacilitator.isForLocale(locale)) {
diff --git a/app/src/main/java/helium314/keyboard/latin/DictionaryFactory.kt b/app/src/main/java/helium314/keyboard/latin/DictionaryFactory.kt
index 576bd90a4..29b416a5d 100644
--- a/app/src/main/java/helium314/keyboard/latin/DictionaryFactory.kt
+++ b/app/src/main/java/helium314/keyboard/latin/DictionaryFactory.kt
@@ -6,102 +6,88 @@
package helium314.keyboard.latin
import android.content.Context
+import helium314.keyboard.latin.common.FileUtils
import helium314.keyboard.latin.common.LocaleUtils
+import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.utils.DictionaryInfoUtils
import helium314.keyboard.latin.utils.Log
import java.io.File
import java.util.LinkedList
import java.util.Locale
-object DictionaryFactory {
- /**
- * Initializes a main dictionary collection for a locale.
- * Uses all dictionaries in cache folder for locale, and adds built-in
- * dictionaries of matching locales if type is not already in cache folder.
- *
- * @return an initialized instance of DictionaryCollection
- */
- // todo:
- // expose the weight so users can adjust dictionary "importance" (useful for addons like emoji dict)
- // allow users to block certain dictionaries (not sure how this should work exactly)
- fun createMainDictionaryCollection(context: Context, locale: Locale): DictionaryCollection {
- val dictList = LinkedList()
- val (extracted, nonExtracted) = getAvailableDictsForLocale(locale, context)
- extracted.sortedBy { !it.name.endsWith(DictionaryInfoUtils.USER_DICTIONARY_SUFFIX) }.forEach {
- // we sort to have user dicts first, so they have priority over internal dicts of the same type
- checkAndAddDictionaryToListNewType(it, dictList, locale)
- }
- nonExtracted.forEach { filename ->
- val type = filename.substringBefore("_")
- if (dictList.any { it.mDictType == type }) return@forEach
- val extractedFile = DictionaryInfoUtils.extractAssetsDictionary(filename, locale, context) ?: return@forEach
- checkAndAddDictionaryToListNewType(extractedFile, dictList, locale)
- }
- return DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList, FloatArray(dictList.size) { 1f })
- }
+/**
+ * Initializes a main dictionary collection from a dictionary pack, with explicit flags.
+ *
+ *
+ * This searches for a content provider providing a dictionary pack for the specified
+ * locale. If none is found, it falls back to the built-in dictionary - if any.
+ * @param context application context for reading resources
+ * @param locale the locale for which to create the dictionary
+ * @return an initialized instance of DictionaryCollection
+ */
+fun createMainDictionary(context: Context, locale: Locale): DictionaryCollection {
+ val cacheDir = DictionaryInfoUtils.getAndCreateCacheDirectoryForLocale(locale, context)
+ val dictList = LinkedList()
+ // get cached dict files
+ val (userDicts, extractedDicts) = DictionaryInfoUtils.getCachedDictsForLocale(locale, context)
+ .partition { it.name.endsWith(DictionaryInfoUtils.USER_DICTIONARY_SUFFIX) }
+ // add user dicts to list
+ userDicts.forEach { checkAndAddDictionaryToListIfNotExisting(it, dictList, locale) }
+ // add extracted dicts to list (after userDicts, to skip extracted dicts of same type)
+ extractedDicts.forEach { checkAndAddDictionaryToListIfNotExisting(it, dictList, locale) }
+ if (dictList.any { it.mDictType == Dictionary.TYPE_MAIN })
+ return DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList)
- fun getAvailableDictsForLocale(locale: Locale, context: Context): Pair, List> {
- val cachedDicts = DictionaryInfoUtils.getCachedDictsForLocale(locale, context)
-
- val nonExtractedDicts = mutableListOf()
- DictionaryInfoUtils.getAssetsDictionaryList(context)
- // file name is _.dict
- ?.groupBy { it.substringBefore("_") }
- ?.forEach { (dictType, dicts) ->
- if (cachedDicts.any { it.name == "$dictType.dict" })
- return@forEach // dictionary is already extracted (can't be old because of cleanup on upgrade)
- val bestMatch = LocaleUtils.getBestMatch(locale, dicts) {
- DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(it)
- } ?: return@forEach
- nonExtractedDicts.add(bestMatch)
- }
- return cachedDicts to nonExtractedDicts
- }
-
- /**
- * add dictionary created from [file] to [dicts]
- * if [file] cannot be loaded it is deleted
- * if the dictionary type already exists in [dicts], the [file] is skipped
- */
- private fun checkAndAddDictionaryToListNewType(file: File, dicts: MutableList, locale: Locale) {
- val dictionary = getDictionary(file, locale) ?: return
- if (dicts.any { it.mDictType == dictionary.mDictType }) {
- dictionary.close()
- return
- }
- dicts.add(dictionary)
- }
-
- @JvmStatic
- fun getDictionary(
- file: File,
- locale: Locale
- ): Dictionary? {
- if (!file.isFile) return null
- val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file)
- if (header == null) {
- killDictionary(file)
- return null
- }
- val dictType = header.mIdString.split(":").first()
- val readOnlyBinaryDictionary = ReadOnlyBinaryDictionary(
- file.absolutePath, 0, file.length(), false, locale, dictType
+ // no main dict found -> check assets
+ val assetsDicts = DictionaryInfoUtils.getAssetsDictionaryList(context)
+ // file name is _.dict
+ val dictsByType = assetsDicts?.groupBy { it.substringBefore("_") }
+ // for each type find the best match
+ dictsByType?.forEach { (dictType, dicts) ->
+ val bestMatch = LocaleUtils.getBestMatch(locale, dicts) { it.substringAfter("_")
+ .substringBefore(".").constructLocale() } ?: return@forEach
+ // extract dict and add extracted file
+ val targetFile = File(cacheDir, "$dictType.dict")
+ FileUtils.copyStreamToNewFile(
+ context.assets.open(DictionaryInfoUtils.ASSETS_DICTIONARY_FOLDER + File.separator + bestMatch),
+ targetFile
)
+ checkAndAddDictionaryToListIfNotExisting(targetFile, dictList, locale)
+ }
+ // If the list is empty, that means we should not use any dictionary (for example, the user
+ // explicitly disabled the main dictionary), so the following is okay. dictList is never
+ // null, but if for some reason it is, DictionaryCollection handles it gracefully.
+ return DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList)
+}
- if (readOnlyBinaryDictionary.isValidDictionary) {
- if (locale.language == "ko") {
- // Use KoreanDictionary for Korean locale
- return KoreanDictionary(readOnlyBinaryDictionary)
- }
- return readOnlyBinaryDictionary
+/**
+ * add dictionary created from [file] to [dicts]
+ * if [file] cannot be loaded it is deleted
+ * if the dictionary type already exists in [dicts], the [file] is skipped
+ */
+private fun checkAndAddDictionaryToListIfNotExisting(file: File, dicts: MutableList, locale: Locale) {
+ if (!file.isFile) return
+ val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file) ?: return killDictionary(file)
+ val dictType = header.mIdString.split(":").first()
+ if (dicts.any { it.mDictType == dictType }) return
+ val readOnlyBinaryDictionary = ReadOnlyBinaryDictionary(
+ file.absolutePath, 0, file.length(), false, locale, dictType
+ )
+
+ if (readOnlyBinaryDictionary.isValidDictionary) {
+ if (locale.language == "ko") {
+ // Use KoreanDictionary for Korean locale
+ dicts.add(KoreanDictionary(readOnlyBinaryDictionary))
+ } else {
+ dicts.add(readOnlyBinaryDictionary)
}
+ } else {
readOnlyBinaryDictionary.close()
killDictionary(file)
- return null
- }
-
- private fun killDictionary(file: File) {
- Log.e("DictionaryFactory", "could not load dictionary ${file.parentFile?.name}/${file.name}, deleting")
- file.delete()
}
}
+
+private fun killDictionary(file: File) {
+ Log.e("DictionaryFactory", "could not load dictionary ${file.parentFile?.name}/${file.name}, deleting")
+ file.delete()
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/EmojiAltPhysicalKeyDetector.java b/app/src/main/java/helium314/keyboard/latin/EmojiAltPhysicalKeyDetector.java
index 9b06420ee..79bfeeb24 100644
--- a/app/src/main/java/helium314/keyboard/latin/EmojiAltPhysicalKeyDetector.java
+++ b/app/src/main/java/helium314/keyboard/latin/EmojiAltPhysicalKeyDetector.java
@@ -23,7 +23,7 @@ import java.util.List;
/**
* A class for detecting Emoji-Alt physical key.
*/
-public final class EmojiAltPhysicalKeyDetector {
+final class EmojiAltPhysicalKeyDetector {
private static final String TAG = "EmojiAltPhysKeyDetector";
private static final boolean DEBUG = false;
diff --git a/app/src/main/java/helium314/keyboard/latin/InputAttributes.java b/app/src/main/java/helium314/keyboard/latin/InputAttributes.java
index 51aae327c..a8b51659a 100644
--- a/app/src/main/java/helium314/keyboard/latin/InputAttributes.java
+++ b/app/src/main/java/helium314/keyboard/latin/InputAttributes.java
@@ -103,7 +103,7 @@ public final class InputAttributes {
|| InputTypeUtils.isEmailVariation(variation)
|| hasNoMicrophoneKeyOption()
|| !RichInputMethodManager.isInitialized() // avoid crash when only using spell checker
- || !RichInputMethodManager.getInstance().isShortcutImeReady();
+ || !RichInputMethodManager.getInstance().hasShortcutIme();
mShouldShowVoiceInputKey = !noMicrophone;
mDisableGestureFloatingPreviewText = InputAttributes.inPrivateImeOptions(
diff --git a/app/src/main/java/helium314/keyboard/latin/InputView.java b/app/src/main/java/helium314/keyboard/latin/InputView.java
index 9ccedb806..8125edc09 100644
--- a/app/src/main/java/helium314/keyboard/latin/InputView.java
+++ b/app/src/main/java/helium314/keyboard/latin/InputView.java
@@ -19,10 +19,8 @@ import helium314.keyboard.accessibility.AccessibilityUtils;
import helium314.keyboard.keyboard.MainKeyboardView;
import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.settings.Settings;
-import helium314.keyboard.latin.suggestions.MoreSuggestionsView;
+import helium314.keyboard.latin.suggestions.PopupSuggestionsView;
import helium314.keyboard.latin.suggestions.SuggestionStripView;
-import kotlin.Unit;
-
public final class InputView extends FrameLayout {
private final Rect mInputViewRect = new Rect();
@@ -45,7 +43,10 @@ public final class InputView extends FrameLayout {
mMainKeyboardView, suggestionStripView);
mMoreSuggestionsViewCanceler = new MoreSuggestionsViewCanceler(
mMainKeyboardView, suggestionStripView);
- ViewKt.doOnNextLayout(this, this::onNextLayout);
+ ViewKt.doOnNextLayout(this, v -> {
+ Settings.getValues().mColors.setBackground(findViewById(R.id.main_keyboard_frame), ColorType.MAIN_BACKGROUND);
+ return null;
+ });
}
public void setKeyboardTopPadding(final int keyboardTopPadding) {
@@ -103,14 +104,6 @@ public final class InputView extends FrameLayout {
return mActiveForwarder.onTouchEvent(x, y, me);
}
- private Unit onNextLayout(View v) {
- Settings.getValues().mColors.setBackground(findViewById(R.id.main_keyboard_frame), ColorType.MAIN_BACKGROUND);
-
- // Work around inset application being unreliable
- requestApplyInsets();
- return null;
- }
-
/**
* This class forwards series of {@link MotionEvent}s from SenderView
to
* ReceiverView
.
@@ -230,8 +223,8 @@ public final class InputView extends FrameLayout {
/**
* This class forwards {@link MotionEvent}s happened in the {@link MainKeyboardView} to
- * {@link SuggestionStripView} when the {@link MoreSuggestionsView} is showing.
- * {@link SuggestionStripView} dismisses {@link MoreSuggestionsView} when it receives any event
+ * {@link SuggestionStripView} when the {@link PopupSuggestionsView} is showing.
+ * {@link SuggestionStripView} dismisses {@link PopupSuggestionsView} when it receives any event
* outside of it.
*/
private static class MoreSuggestionsViewCanceler
diff --git a/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt b/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt
index ab5998348..5fb5a122d 100644
--- a/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt
+++ b/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt
@@ -78,13 +78,12 @@ class KeyboardWrapperView @JvmOverloads constructor(
val changePercent = 2 * sign * (x - motionEvent.rawX) / context.resources.displayMetrics.density
if (abs(changePercent) < 1) return@setOnTouchListener true
x = motionEvent.rawX
- val landscape = Settings.getValues().mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE
- val split = Settings.getValues().mIsSplitKeyboardEnabled
- val oldScale = Settings.readOneHandedModeScale(context.prefs(), landscape, split)
+ val oldScale = Settings.readOneHandedModeScale(context.prefs(), Settings.getValues().mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT)
val newScale = (oldScale + changePercent / 100f).coerceAtMost(2.5f).coerceAtLeast(0.5f)
if (newScale == oldScale) return@setOnTouchListener true
Settings.getInstance().writeOneHandedModeScale(newScale)
- KeyboardSwitcher.getInstance().setOneHandedModeEnabled(true, true)
+ oneHandedModeEnabled = false // intentionally putting wrong value, so KeyboardSwitcher.setOneHandedModeEnabled does actually reload
+ KeyboardSwitcher.getInstance().setOneHandedModeEnabled(true)
}
else -> x = 0f
}
diff --git a/app/src/main/java/helium314/keyboard/latin/KoreanDictionary.java b/app/src/main/java/helium314/keyboard/latin/KoreanDictionary.java
index 2b6722777..9b791148c 100644
--- a/app/src/main/java/helium314/keyboard/latin/KoreanDictionary.java
+++ b/app/src/main/java/helium314/keyboard/latin/KoreanDictionary.java
@@ -4,7 +4,6 @@ package helium314.keyboard.latin;
import helium314.keyboard.event.HangulCombiner;
import helium314.keyboard.latin.common.ComposedData;
-import helium314.keyboard.latin.makedict.WordProperty;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
import java.text.Normalizer;
@@ -73,11 +72,6 @@ public class KoreanDictionary extends Dictionary {
return mDictionary.getMaxFrequencyOfExactMatches(processInput(word));
}
- @Override
- public WordProperty getWordProperty(String word, boolean isBeginningOfSentence) {
- return mDictionary.getWordProperty(processInput(word), isBeginningOfSentence);
- }
-
@Override
protected boolean same(char[] word, int length, String typedWord) {
word = processInput(new String(word)).toCharArray();
diff --git a/app/src/main/java/helium314/keyboard/latin/LatinIME.java b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
index 9e2ed3c72..5913bf701 100644
--- a/app/src/main/java/helium314/keyboard/latin/LatinIME.java
+++ b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
@@ -26,6 +26,7 @@ import android.os.Process;
import android.text.InputType;
import android.util.PrintWriterPrinter;
import android.util.Printer;
+import android.util.SparseArray;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
@@ -50,12 +51,16 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.common.InsetsOutlineProvider;
import helium314.keyboard.dictionarypack.DictionaryPackConstants;
import helium314.keyboard.event.Event;
+import helium314.keyboard.event.HangulEventDecoder;
+import helium314.keyboard.event.HardwareEventDecoder;
+import helium314.keyboard.event.HardwareKeyboardEventDecoder;
import helium314.keyboard.event.InputTransaction;
import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.keyboard.KeyboardId;
import helium314.keyboard.keyboard.KeyboardLayoutSet;
import helium314.keyboard.keyboard.KeyboardSwitcher;
import helium314.keyboard.keyboard.MainKeyboardView;
+import helium314.keyboard.latin.Suggest.OnGetSuggestedWordsCallback;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.common.Constants;
@@ -64,6 +69,7 @@ import helium314.keyboard.latin.common.InputPointers;
import helium314.keyboard.latin.common.LocaleUtils;
import helium314.keyboard.latin.common.ViewOutlineProviderUtilsKt;
import helium314.keyboard.latin.define.DebugFlags;
+import helium314.keyboard.latin.define.ProductionFlags;
import helium314.keyboard.latin.inputlogic.InputLogic;
import helium314.keyboard.latin.personalization.PersonalizationHelper;
import helium314.keyboard.latin.settings.Settings;
@@ -81,7 +87,6 @@ import helium314.keyboard.latin.utils.StatsUtils;
import helium314.keyboard.latin.utils.StatsUtilsManager;
import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
import helium314.keyboard.latin.utils.SubtypeSettings;
-import helium314.keyboard.latin.utils.ToolbarMode;
import helium314.keyboard.latin.utils.ViewLayoutUtils;
import helium314.keyboard.settings.SettingsActivity;
import kotlin.collections.CollectionsKt;
@@ -123,12 +128,12 @@ public class LatinIME extends InputMethodService implements
public final KeyboardActionListener mKeyboardActionListener;
private int mOriginalNavBarColor = 0;
private int mOriginalNavBarFlags = 0;
-
- // UIHandler is needed when creating InputLogic
- public final UIHandler mHandler = new UIHandler(this);
private final DictionaryFacilitator mDictionaryFacilitator =
DictionaryFacilitatorProvider.getDictionaryFacilitator(false);
final InputLogic mInputLogic = new InputLogic(this, this, mDictionaryFacilitator);
+ // We expect to have only one decoder in almost all cases, hence the default capacity of 1.
+ // If it turns out we need several, it will get grown seamlessly.
+ final SparseArray mHardwareEventDecoders = new SparseArray<>(1);
// TODO: Move these {@link View}s to {@link KeyboardSwitcher}.
private View mInputView;
@@ -138,6 +143,7 @@ public class LatinIME extends InputMethodService implements
private RichInputMethodManager mRichImm;
final KeyboardSwitcher mKeyboardSwitcher;
private final SubtypeState mSubtypeState = new SubtypeState();
+ private EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector;
private final StatsUtilsManager mStatsUtilsManager;
// Working variable for {@link #startShowingInputView()} and
// {@link #onEvaluateInputViewShown()}.
@@ -180,6 +186,8 @@ public class LatinIME extends InputMethodService implements
private final ClipboardHistoryManager mClipboardHistoryManager = new ClipboardHistoryManager(this);
+ public final UIHandler mHandler = new UIHandler(this);
+
public static final class UIHandler extends LeakGuardHandlerWrapper {
private static final int MSG_UPDATE_SHIFT_STATE = 0;
private static final int MSG_PENDING_IMS_CALLBACK = 1;
@@ -270,7 +278,9 @@ public class LatinIME extends InputMethodService implements
msg.arg2 /* remainingTries */, this /* handler */)) {
// If we were able to reset the caches, then we can reload the keyboard.
// Otherwise, we'll do it when we can.
- latinIme.mKeyboardSwitcher.reloadMainKeyboard();
+ latinIme.mKeyboardSwitcher.loadKeyboard(latinIme.getCurrentInputEditorInfo(),
+ settingsValues, latinIme.getCurrentAutoCapsState(),
+ latinIme.getCurrentRecapitalizeState());
}
break;
case MSG_WAIT_FOR_DICTIONARY_LOAD:
@@ -598,7 +608,7 @@ public class LatinIME extends InputMethodService implements
mCurrentSubtypeHasBeenUsed = false;
}
if (currentSubtypeHasBeenUsed
- && SubtypeSettings.INSTANCE.isEnabled(lastActiveSubtype)
+ && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype)
&& !currentSubtype.equals(lastActiveSubtype)) {
switchToSubtype(lastActiveSubtype);
return;
@@ -626,7 +636,6 @@ public class LatinIME extends InputMethodService implements
@Override
public void onCreate() {
- mSettings.startListener();
KeyboardIconsSet.Companion.getInstance().loadIcons(this);
mRichImm = RichInputMethodManager.getInstance();
AudioAndHapticFeedbackManager.init(this);
@@ -654,8 +663,7 @@ public class LatinIME extends InputMethodService implements
final IntentFilter newDictFilter = new IntentFilter();
newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
- // RECEIVER_EXPORTED is necessary because apparently Android 15 (and others?) don't recognize if the sender and receiver are the same app, see https://github.com/Helium314/HeliBoard/pull/1756
- ContextCompat.registerReceiver(this, mDictionaryPackInstallReceiver, newDictFilter, ContextCompat.RECEIVER_EXPORTED);
+ ContextCompat.registerReceiver(this, mDictionaryPackInstallReceiver, newDictFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
final IntentFilter dictDumpFilter = new IntentFilter();
dictDumpFilter.addAction(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION);
@@ -729,8 +737,8 @@ public class LatinIME extends InputMethodService implements
if (mDictionaryFacilitator.usesSameSettings(
locales,
mSettings.getCurrent().mUseContactsDictionary,
- mSettings.getCurrent().mUseAppsDictionary,
- mSettings.getCurrent().mUsePersonalizedDicts
+ mSettings.getCurrent().mUsePersonalizedDicts,
+ mSettings.getCurrent().mAccount
)) {
return;
}
@@ -746,14 +754,9 @@ public class LatinIME extends InputMethodService implements
// TODO: make sure the current settings always have the right locales, and read from them.
private void resetDictionaryFacilitator(@NonNull final Locale locale) {
final SettingsValues settingsValues = mSettings.getCurrent();
- try {
- mDictionaryFacilitator.resetDictionaries(this, locale,
- settingsValues.mUseContactsDictionary, settingsValues.mUseAppsDictionary,
- settingsValues.mUsePersonalizedDicts, false, "", this);
- } catch (Throwable e) {
- // this should not happen, but in case it does we at least want to show a keyboard
- Log.e(TAG, "Could not reset dictionary facilitator, please fix ASAP", e);
- }
+ mDictionaryFacilitator.resetDictionaries(this, locale,
+ settingsValues.mUseContactsDictionary, settingsValues.mUsePersonalizedDicts,
+ false, settingsValues.mAccount, "", this);
mInputLogic.mSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold);
}
@@ -762,9 +765,12 @@ public class LatinIME extends InputMethodService implements
*/
/* package private */ void resetSuggestMainDict() {
final SettingsValues settingsValues = mSettings.getCurrent();
- mDictionaryFacilitator.resetDictionaries(this, mDictionaryFacilitator.getMainLocale(),
- settingsValues.mUseContactsDictionary, settingsValues.mUseAppsDictionary,
- settingsValues.mUsePersonalizedDicts, true, "", this);
+ mDictionaryFacilitator.resetDictionaries(this /* context */,
+ mDictionaryFacilitator.getMainLocale(), settingsValues.mUseContactsDictionary,
+ settingsValues.mUsePersonalizedDicts,
+ true /* forceReloadMainDictionary */,
+ settingsValues.mAccount, "" /* dictNamePrefix */,
+ this /* DictionaryInitializationListener */);
}
// used for debug
@@ -787,6 +793,14 @@ public class LatinIME extends InputMethodService implements
deallocateMemory();
}
+ public void recycle() {
+ unregisterReceiver(mDictionaryPackInstallReceiver);
+ unregisterReceiver(mDictionaryDumpBroadcastReceiver);
+ unregisterReceiver(mRingerModeChangeReceiver);
+ unregisterReceiver(mRestartAfterDeviceUnlockReceiver);
+ mInputLogic.recycle();
+ }
+
private boolean isImeSuppressedByHardwareKeyboard() {
final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance();
return !onEvaluateInputViewShown() && switcher.isImeSuppressedByHardwareKeyboard(
@@ -868,8 +882,7 @@ public class LatinIME extends InputMethodService implements
mInputView = view;
mInsetsUpdater = ViewOutlineProviderUtilsKt.setInsetsOutlineProvider(view);
updateSoftInputWindowLayoutParameters();
- mSuggestionStripView = mSettings.getCurrent().mToolbarMode == ToolbarMode.HIDDEN?
- null : view.findViewById(R.id.suggestion_strip_view);
+ mSuggestionStripView = view.findViewById(R.id.suggestion_strip_view);
if (hasSuggestionStripView()) {
mSuggestionStripView.setListener(this, view);
}
@@ -921,9 +934,8 @@ public class LatinIME extends InputMethodService implements
mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype),
mSettings.getCurrent());
loadKeyboard();
- if (hasSuggestionStripView()) {
+ if (mSuggestionStripView != null)
mSuggestionStripView.setRtl(mRichImm.getCurrentSubtype().isRtlSubtype());
- }
}
/** alias to onCurrentInputMethodSubtypeChanged with a better name, as it's also used for internal switching */
@@ -1011,8 +1023,6 @@ public class LatinIME extends InputMethodService implements
!currentSettingsValues.hasSameOrientation(getResources().getConfiguration())) {
loadSettings();
currentSettingsValues = mSettings.getCurrent();
- if (hasSuggestionStripView())
- mSuggestionStripView.updateVoiceKey();
}
// ALERT: settings have not been reloaded and there is a chance they may be stale.
// In the practice, if it is, we should have gotten onConfigurationChanged so it should
@@ -1062,7 +1072,7 @@ public class LatinIME extends InputMethodService implements
if (isDifferentTextField) {
mainKeyboardView.closing();
suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold);
- switcher.reloadMainKeyboard();
+ switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(), getCurrentRecapitalizeState());
if (needToCallLoadKeyboardLater) {
// If we need to call loadKeyboard again later, we need to save its state now. The
// later call will be done in #retryResetCaches.
@@ -1110,7 +1120,6 @@ public class LatinIME extends InputMethodService implements
@Override
public void onWindowHidden() {
super.onWindowHidden();
- Log.i(TAG, "onWindowHidden");
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
if (mainKeyboardView != null) {
mainKeyboardView.closing();
@@ -1167,12 +1176,8 @@ public class LatinIME extends InputMethodService implements
if (isInputViewShown()
&& mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
composingSpanStart, composingSpanEnd, settingsValues)) {
- // we don't want to update a manually set shift state if selection changed towards one side
- // because this may end the manual shift, which is unwanted in case of shift + arrow keys for changing selection
- // todo: this is not fully implemented yet, and maybe should be behind a setting
- if (mKeyboardSwitcher.getKeyboard() != null && mKeyboardSwitcher.getKeyboard().mId.isAlphabetShiftedManually()
- && !((oldSelEnd == newSelEnd && oldSelStart != newSelStart) || (oldSelEnd != newSelEnd && oldSelStart == newSelStart)))
- mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState());
+ mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(),
+ getCurrentRecapitalizeState());
}
}
@@ -1213,8 +1218,7 @@ public class LatinIME extends InputMethodService implements
@Override
public void hideWindow() {
- Log.i(TAG, "hideWindow");
- if (hasSuggestionStripView() && mSettings.getCurrent().mToolbarMode == ToolbarMode.EXPANDABLE)
+ if (mSuggestionStripView != null)
mSuggestionStripView.setToolbarVisibility(false);
mKeyboardSwitcher.onHideWindow();
@@ -1226,12 +1230,6 @@ public class LatinIME extends InputMethodService implements
super.hideWindow();
}
- @Override
- public void requestHideSelf(int flags) {
- super.requestHideSelf(flags);
- Log.i(TAG, "requestHideSelf: " + flags);
- }
-
@Override
public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
if (DebugFlags.DEBUG_ENABLED) {
@@ -1276,25 +1274,20 @@ public class LatinIME extends InputMethodService implements
return;
}
final View visibleKeyboardView = mKeyboardSwitcher.getWrapperView();
- if (visibleKeyboardView == null) {
+ if (visibleKeyboardView == null || !hasSuggestionStripView()) {
return;
}
final int inputHeight = mInputView.getHeight();
if (isImeSuppressedByHardwareKeyboard() && !visibleKeyboardView.isShown()) {
// If there is a hardware keyboard and a visible software keyboard view has been hidden,
// no visual element will be shown on the screen.
- // for some reason setting contentTopInsets and visibleTopInsets broke somewhere along the
- // way from OpenBoard to HeliBoard (GH-702, GH-1455), but not setting anything seems to work
+ outInsets.contentTopInsets = inputHeight;
+ outInsets.visibleTopInsets = inputHeight;
mInsetsUpdater.setInsets(outInsets);
return;
}
- final int stripHeight = mKeyboardSwitcher.isShowingStripContainer() ? mKeyboardSwitcher.getStripContainer().getHeight() : 0;
- final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - stripHeight;
-
- if (hasSuggestionStripView()) {
- mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY);
- }
-
+ final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - mSuggestionStripView.getHeight();
+ mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY);
// Need to set expanded touchable region only if a keyboard view is being shown.
if (visibleKeyboardView.isShown()) {
final int touchLeft = 0;
@@ -1378,10 +1371,6 @@ public class LatinIME extends InputMethodService implements
@RequiresApi(api = Build.VERSION_CODES.R)
public InlineSuggestionsRequest onCreateInlineSuggestionsRequest(@NonNull Bundle uiExtras) {
Log.d(TAG,"onCreateInlineSuggestionsRequest called");
- if (Settings.getValues().mSuggestionStripHiddenPerUserSettings) {
- return null;
- }
-
return InlineAutofillUtils.createInlineSuggestionRequest(mDisplayContext);
}
@@ -1389,10 +1378,6 @@ public class LatinIME extends InputMethodService implements
@RequiresApi(api = Build.VERSION_CODES.R)
public boolean onInlineSuggestionsResponse(InlineSuggestionsResponse response) {
Log.d(TAG,"onInlineSuggestionsResponse called");
- if (Settings.getValues().mSuggestionStripHiddenPerUserSettings) {
- return false;
- }
-
final List inlineSuggestions = response.getInlineSuggestions();
if (inlineSuggestions.isEmpty()) {
return false;
@@ -1479,7 +1464,7 @@ public class LatinIME extends InputMethodService implements
// switch IME if wanted and possible
if (switchIme && !switchSubtype && switchInputMethod())
return;
- final boolean hasMoreThanOneSubtype = mRichImm.hasMultipleEnabledSubtypesInThisIme(true);
+ final boolean hasMoreThanOneSubtype = mRichImm.getMyEnabledInputMethodSubtypeList(false).size() > 1;
// switch subtype if wanted, do nothing if no other subtype is available
if (switchSubtype && !switchIme) {
if (hasMoreThanOneSubtype)
@@ -1542,7 +1527,25 @@ public class LatinIME extends InputMethodService implements
// Implementation of {@link SuggestionStripView.Listener}.
@Override
public void onCodeInput(final int codePoint, final int x, final int y, final boolean isKeyRepeat) {
- mKeyboardActionListener.onCodeInput(codePoint, x, y, isKeyRepeat);
+ onCodeInput(codePoint, 0, x, y, isKeyRepeat);
+ }
+
+ public void onCodeInput(final int codePoint, final int metaState, final int x, final int y, final boolean isKeyRepeat) {
+ if (codePoint < 0) {
+ switch (codePoint) {
+ case KeyCode.TOGGLE_AUTOCORRECT -> {mSettings.toggleAutoCorrect(); return; }
+ case KeyCode.TOGGLE_INCOGNITO_MODE -> {mSettings.toggleAlwaysIncognitoMode(); return; }
+ }
+ }
+ final Event event;
+ // checking if the character is a combining accent
+ if (0x300 <= codePoint && codePoint <= 0x35b) {
+ event = Event.createSoftwareDeadEvent(codePoint, 0, metaState, x, y, null);
+ } else {
+ event = createSoftwareKeypressEvent(codePoint, metaState, x, y, isKeyRepeat);
+ }
+
+ onEvent(event);
}
// This method is public for testability of LatinIME, but also in the future it should
@@ -1559,6 +1562,24 @@ public class LatinIME extends InputMethodService implements
mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState());
}
+ // A helper method to split the code point and the key code. Ultimately, they should not be
+ // squashed into the same variable, and this method should be removed.
+ // public for testing, as we don't want to copy the same logic into test code
+ @NonNull
+ public static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int metaState,
+ final int keyX, final int keyY, final boolean isKeyRepeat) {
+ final int keyCode;
+ final int codePoint;
+ if (keyCodeOrCodePoint <= 0) {
+ keyCode = keyCodeOrCodePoint;
+ codePoint = Event.NOT_A_CODE_POINT;
+ } else {
+ keyCode = Event.NOT_A_KEY_CODE;
+ codePoint = keyCodeOrCodePoint;
+ }
+ return Event.createSoftwareKeypressEvent(codePoint, keyCode, metaState, keyX, keyY, isKeyRepeat);
+ }
+
public void onTextInput(final String rawText) {
// TODO: have the keyboard pass the correct key code when we need it.
final Event event = Event.createSoftwareTextEvent(rawText, KeyCode.MULTIPLE_CODE_POINTS);
@@ -1611,7 +1632,7 @@ public class LatinIME extends InputMethodService implements
dismissGestureFloatingPreviewText /* dismissDelayed */);
}
- private boolean hasSuggestionStripView() {
+ public boolean hasSuggestionStripView() {
return null != mSuggestionStripView;
}
@@ -1646,6 +1667,18 @@ public class LatinIME extends InputMethodService implements
}
}
+ // TODO[IL]: Move this out of LatinIME.
+ public void getSuggestedWords(final int inputStyle, final int sequenceNumber,
+ final OnGetSuggestedWordsCallback callback) {
+ final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
+ if (keyboard == null) {
+ callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance());
+ return;
+ }
+ mInputLogic.getSuggestedWords(mSettings.getCurrent(), keyboard,
+ mKeyboardSwitcher.getKeyboardShiftMode(), inputStyle, sequenceNumber, callback);
+ }
+
@Override
public void showSuggestionStrip(final SuggestedWords suggestedWords) {
if (suggestedWords.isEmpty()) {
@@ -1730,7 +1763,8 @@ public class LatinIME extends InputMethodService implements
loadSettings();
if (mKeyboardSwitcher.getMainKeyboardView() != null) {
// Reload keyboard because the current language has been changed.
- mKeyboardSwitcher.reloadMainKeyboard();
+ mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent(),
+ getCurrentAutoCapsState(), getCurrentRecapitalizeState());
}
}
@@ -1798,18 +1832,63 @@ public class LatinIME extends InputMethodService implements
feedbackManager.performAudioFeedback(code);
}
+ private HardwareEventDecoder getHardwareKeyEventDecoder(final int deviceId) {
+ final HardwareEventDecoder decoder = mHardwareEventDecoders.get(deviceId);
+ if (null != decoder) return decoder;
+ // TODO: create the decoder according to the specification
+ final HardwareEventDecoder newDecoder = new HardwareKeyboardEventDecoder(deviceId);
+ mHardwareEventDecoders.put(deviceId, newDecoder);
+ return newDecoder;
+ }
+
// Hooks for hardware keyboard
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
- if (mKeyboardActionListener.onKeyDown(keyCode, keyEvent))
+ if (mEmojiAltPhysicalKeyDetector == null) {
+ mEmojiAltPhysicalKeyDetector = new EmojiAltPhysicalKeyDetector(
+ getApplicationContext().getResources());
+ }
+ mEmojiAltPhysicalKeyDetector.onKeyDown(keyEvent);
+ if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
+ return super.onKeyDown(keyCode, keyEvent);
+ }
+ final Event event;
+ if (mRichImm.getCurrentSubtypeLocale().getLanguage().equals("ko")) {
+ final RichInputMethodSubtype subtype = mKeyboardSwitcher.getKeyboard() == null
+ ? mRichImm.getCurrentSubtype()
+ : mKeyboardSwitcher.getKeyboard().mId.mSubtype;
+ event = HangulEventDecoder.decodeHardwareKeyEvent(subtype, keyEvent,
+ () -> getHardwareKeyEventDecoder(keyEvent.getDeviceId()).decodeHardwareKey(keyEvent));
+ } else {
+ event = getHardwareKeyEventDecoder(keyEvent.getDeviceId()).decodeHardwareKey(keyEvent);
+ }
+ // If the event is not handled by LatinIME, we just pass it to the parent implementation.
+ // If it's handled, we return true because we did handle it.
+ if (event.isHandled()) {
+ mInputLogic.onCodeInput(mSettings.getCurrent(), event,
+ mKeyboardSwitcher.getKeyboardShiftMode(),
+ // TODO: this is not necessarily correct for a hardware keyboard right now
+ mKeyboardSwitcher.getCurrentKeyboardScript(),
+ mHandler);
return true;
+ }
return super.onKeyDown(keyCode, keyEvent);
}
@Override
public boolean onKeyUp(final int keyCode, final KeyEvent keyEvent) {
- if (mKeyboardActionListener.onKeyUp(keyCode, keyEvent))
+ if (mEmojiAltPhysicalKeyDetector == null) {
+ mEmojiAltPhysicalKeyDetector = new EmojiAltPhysicalKeyDetector(
+ getApplicationContext().getResources());
+ }
+ mEmojiAltPhysicalKeyDetector.onKeyUp(keyEvent);
+ if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
+ return super.onKeyUp(keyCode, keyEvent);
+ }
+ final long keyIdentifier = (long) keyEvent.getDeviceId() << 32 + keyEvent.getKeyCode();
+ if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
return true;
+ }
return super.onKeyUp(keyCode, keyEvent);
}
@@ -1942,10 +2021,8 @@ public class LatinIME extends InputMethodService implements
public void onTrimMemory(int level) {
super.onTrimMemory(level);
switch (level) {
- case TRIM_MEMORY_RUNNING_LOW, TRIM_MEMORY_RUNNING_CRITICAL, TRIM_MEMORY_COMPLETE -> {
- KeyboardLayoutSet.onSystemLocaleChanged(); // clears caches, nothing else
- mKeyboardSwitcher.trimMemory();
- }
+ case TRIM_MEMORY_RUNNING_LOW, TRIM_MEMORY_RUNNING_CRITICAL, TRIM_MEMORY_COMPLETE ->
+ KeyboardLayoutSet.onSystemLocaleChanged(); // clears caches, nothing else
// deallocateMemory always called on hiding, and should not be called when showing
}
}
diff --git a/app/src/main/java/helium314/keyboard/latin/ReadOnlyBinaryDictionary.java b/app/src/main/java/helium314/keyboard/latin/ReadOnlyBinaryDictionary.java
index f757e87eb..b4cfe5f3a 100644
--- a/app/src/main/java/helium314/keyboard/latin/ReadOnlyBinaryDictionary.java
+++ b/app/src/main/java/helium314/keyboard/latin/ReadOnlyBinaryDictionary.java
@@ -10,7 +10,6 @@ import com.android.inputmethod.latin.BinaryDictionary;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
-import helium314.keyboard.latin.makedict.WordProperty;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
import java.util.ArrayList;
@@ -108,18 +107,6 @@ public final class ReadOnlyBinaryDictionary extends Dictionary {
return NOT_A_PROBABILITY;
}
- @Override
- public WordProperty getWordProperty(String word, boolean isBeginningOfSentence) {
- if (mLock.readLock().tryLock()) {
- try {
- return mBinaryDictionary.getWordProperty(word, isBeginningOfSentence);
- } finally {
- mLock.readLock().unlock();
- }
- }
- return null;
- }
-
@Override
public void close() {
mLock.writeLock().lock();
diff --git a/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java b/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java
index f7e8bcc03..3d81249b1 100644
--- a/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java
+++ b/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java
@@ -40,6 +40,8 @@ import helium314.keyboard.latin.settings.SpacingAndPunctuations;
import helium314.keyboard.latin.utils.CapsModeUtils;
import helium314.keyboard.latin.utils.DebugLogUtils;
import helium314.keyboard.latin.utils.NgramContextUtils;
+import helium314.keyboard.latin.utils.ScriptUtils;
+import helium314.keyboard.latin.utils.SpannableStringUtils;
import helium314.keyboard.latin.utils.StatsUtils;
import helium314.keyboard.latin.utils.TextRange;
@@ -439,7 +441,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
// test for this explicitly)
if (INVALID_CURSOR_POSITION != mExpectedSelStart
&& (cachedLength >= n || cachedLength >= mExpectedSelStart)) {
- final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText.toString());
+ final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText);
// We call #toString() here to create a temporary object.
// In some situations, this method is called on a worker thread, and it's possible
// the main thread touches the contents of mComposingText while this worker thread
@@ -716,13 +718,8 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (start < 0 || end < 0) {
return false;
}
- if (start > end) {
- mExpectedSelStart = end;
- mExpectedSelEnd = start;
- } else {
- mExpectedSelStart = start;
- mExpectedSelEnd = end;
- }
+ mExpectedSelStart = start;
+ mExpectedSelEnd = end;
if (isConnected()) {
final boolean isIcValid = mIC.setSelection(start, end);
if (!isIcValid) {
@@ -828,6 +825,15 @@ public final class RichInputConnection implements PrivateCommandPerformer {
return NgramContextUtils.getNgramContextFromNthPreviousWord(prev, spacingAndPunctuations, n);
}
+ private static boolean isPartOfCompositionForScript(final int codePoint,
+ final SpacingAndPunctuations spacingAndPunctuations, final String script) {
+ // We always consider word connectors part of compositions.
+ return spacingAndPunctuations.isWordConnector(codePoint)
+ // Otherwise, it's part of composition if it's part of script and not a separator.
+ || (!spacingAndPunctuations.isWordSeparator(codePoint)
+ && ScriptUtils.isLetterPartOfScript(codePoint, script));
+ }
+
/**
* Returns the text surrounding the cursor.
*
@@ -854,7 +860,90 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (before == null || after == null) {
return null;
}
- return StringUtilsKt.getTouchedWordRange(before, after, script, spacingAndPunctuations);
+
+ // Going backward, find the first breaking point (separator)
+ int startIndexInBefore = before.length();
+ int endIndexInAfter = -1;
+ while (startIndexInBefore > 0) {
+ final int codePoint = Character.codePointBefore(before, startIndexInBefore);
+ if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
+ if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
+ break;
+ // continue to the next whitespace and see whether this contains a sometimesWordConnector
+ for (int i = startIndexInBefore - 1; i >= 0; i--) {
+ final char c = before.charAt(i);
+ if (spacingAndPunctuations.isSometimesWordConnector(c)) {
+ // if yes -> whitespace is the index
+ startIndexInBefore = Math.max(StringUtils.charIndexOfLastWhitespace(before), 0);
+ final int firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after);
+ endIndexInAfter = firstSpaceAfter == -1 ? after.length() : firstSpaceAfter -1;
+ break;
+ } else if (Character.isWhitespace(c)) {
+ // if no, just break normally
+ break;
+ }
+ }
+ break;
+ }
+ --startIndexInBefore;
+ if (Character.isSupplementaryCodePoint(codePoint)) {
+ --startIndexInBefore;
+ }
+ }
+
+ // Find last word separator after the cursor
+ if (endIndexInAfter == -1) {
+ while (++endIndexInAfter < after.length()) {
+ final int codePoint = Character.codePointAt(after, endIndexInAfter);
+ if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
+ if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
+ break;
+ // continue to the next whitespace and see whether this contains a sometimesWordConnector
+ for (int i = endIndexInAfter; i < after.length(); i++) {
+ final char c = after.charAt(i);
+ if (spacingAndPunctuations.isSometimesWordConnector(c)) {
+ // if yes -> whitespace is next to the index
+ startIndexInBefore = Math.max(StringUtils.charIndexOfLastWhitespace(before), 0);
+ final int firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after);
+ endIndexInAfter = firstSpaceAfter == -1 ? after.length() : firstSpaceAfter - 1;
+ break;
+ } else if (Character.isWhitespace(c)) {
+ // if no, just break normally
+ break;
+ }
+ }
+ break;
+ }
+ if (Character.isSupplementaryCodePoint(codePoint)) {
+ ++endIndexInAfter;
+ }
+ }
+ }
+
+ // strip stuff before "//" (i.e. ignore http and other protocols)
+ final String beforeConsideringStart = before.subSequence(startIndexInBefore, before.length()).toString();
+ final int protocolEnd = beforeConsideringStart.lastIndexOf("//");
+ if (protocolEnd != -1)
+ startIndexInBefore += protocolEnd + 1;
+
+ // we don't want the end characters to be word separators
+ while (endIndexInAfter > 0 && spacingAndPunctuations.isWordSeparator(after.charAt(endIndexInAfter - 1))) {
+ --endIndexInAfter;
+ }
+ while (startIndexInBefore < before.length() && spacingAndPunctuations.isWordSeparator(before.charAt(startIndexInBefore))) {
+ ++startIndexInBefore;
+ }
+
+ final boolean hasUrlSpans =
+ SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length())
+ || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter);
+ // We don't use TextUtils#concat because it copies all spans without respect to their
+ // nature. If the text includes a PARAGRAPH span and it has been split, then
+ // TextUtils#concat will crash when it tries to concat both sides of it.
+ return new TextRange(
+ SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after),
+ startIndexInBefore, before.length() + endIndexInAfter, before.length(),
+ hasUrlSpans);
}
public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations,
@@ -867,7 +956,19 @@ public final class RichInputConnection implements PrivateCommandPerformer {
// a composing region should always count as a word
return true;
}
- return StringUtilsKt.endsWithWordCodepoint(mCommittedTextBeforeComposingText.toString(), spacingAndPunctuations);
+ final String textBeforeCursor = mCommittedTextBeforeComposingText.toString();
+ int indexOfCodePointInJavaChars = textBeforeCursor.length();
+ int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
+ : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
+ // Search for the first non word-connector char
+ if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) {
+ indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint);
+ consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
+ : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
+ }
+ return !(Constants.NOT_A_CODE == consideredCodePoint
+ || spacingAndPunctuations.isWordSeparator(consideredCodePoint)
+ || spacingAndPunctuations.isWordConnector(consideredCodePoint));
}
public boolean isCursorFollowedByWordCharacter(
diff --git a/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java b/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java
new file mode 100644
index 000000000..607e30a92
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java
@@ -0,0 +1,446 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.latin;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.inputmethodservice.InputMethodService;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodSubtype;
+
+import helium314.keyboard.compat.ConfigurationCompatKt;
+import helium314.keyboard.latin.common.LocaleUtils;
+import helium314.keyboard.latin.settings.Settings;
+import helium314.keyboard.latin.utils.KtxKt;
+import helium314.keyboard.latin.utils.LanguageOnSpacebarUtils;
+import helium314.keyboard.latin.utils.Log;
+import helium314.keyboard.latin.utils.ScriptUtils;
+import helium314.keyboard.latin.utils.SubtypeLocaleUtils;
+import helium314.keyboard.latin.utils.SubtypeSettings;
+import helium314.keyboard.latin.utils.SubtypeUtilsKt;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static helium314.keyboard.latin.common.Constants.Subtype.KEYBOARD_MODE;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Enrichment class for InputMethodManager to simplify interaction and add functionality.
+ */
+// non final for easy mocking.
+public class RichInputMethodManager {
+ private static final String TAG = RichInputMethodManager.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private RichInputMethodManager() {
+ // This utility class is not publicly instantiable.
+ }
+
+ private static final RichInputMethodManager sInstance = new RichInputMethodManager();
+
+ private Context mContext;
+ private InputMethodManager mImm;
+ private InputMethodInfoCache mInputMethodInfoCache;
+ private RichInputMethodSubtype mCurrentRichInputMethodSubtype;
+ private InputMethodInfo mShortcutInputMethodInfo;
+ private InputMethodSubtype mShortcutSubtype;
+
+ private static final int INDEX_NOT_FOUND = -1;
+
+ public static RichInputMethodManager getInstance() {
+ sInstance.checkInitialized();
+ return sInstance;
+ }
+
+ public static void init(final Context context) {
+ sInstance.initInternal(context);
+ }
+
+ private boolean isInitializedInternal() {
+ return mImm != null;
+ }
+
+ public static boolean isInitialized() {
+ return sInstance.isInitializedInternal();
+ }
+
+ private void checkInitialized() {
+ if (!isInitializedInternal()) {
+ throw new RuntimeException(TAG + " is used before initialization");
+ }
+ }
+
+ private void initInternal(final Context context) {
+ if (isInitializedInternal()) {
+ return;
+ }
+ mImm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ mContext = context;
+ mInputMethodInfoCache = new InputMethodInfoCache(mImm, context.getPackageName());
+
+ // Initialize subtype utils.
+ SubtypeLocaleUtils.init(context);
+
+ // Initialize the current input method subtype and the shortcut IME.
+ refreshSubtypeCaches();
+ }
+
+ public InputMethodManager getInputMethodManager() {
+ checkInitialized();
+ return mImm;
+ }
+
+ public List getMyEnabledInputMethodSubtypeList(
+ boolean allowsImplicitlySelectedSubtypes) {
+ return getEnabledInputMethodSubtypeList(
+ getInputMethodInfoOfThisIme(), allowsImplicitlySelectedSubtypes);
+ }
+
+ public @Nullable InputMethodSubtype getNextSubtypeInThisIme(final boolean onlyCurrentIme) {
+ final InputMethodSubtype currentSubtype = getCurrentSubtype().getRawSubtype();
+ final List enabledSubtypes = getMyEnabledInputMethodSubtypeList(true);
+ final int currentIndex = enabledSubtypes.indexOf(currentSubtype);
+ if (currentIndex == INDEX_NOT_FOUND) {
+ Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
+ + SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype));
+ if (onlyCurrentIme) return enabledSubtypes.get(0); // just return first enabled subtype
+ else return null;
+ }
+ final int nextIndex = (currentIndex + 1) % enabledSubtypes.size();
+ if (nextIndex <= currentIndex && !onlyCurrentIme) {
+ // The current subtype is the last or only enabled one and it needs to switch to next IME.
+ return null;
+ }
+ return enabledSubtypes.get(nextIndex);
+ }
+
+ private static class InputMethodInfoCache {
+ private final InputMethodManager mImm;
+ private final String mImePackageName;
+
+ private InputMethodInfo mCachedThisImeInfo;
+ private final HashMap>
+ mCachedSubtypeListWithImplicitlySelected;
+ private final HashMap>
+ mCachedSubtypeListOnlyExplicitlySelected;
+
+ public InputMethodInfoCache(final InputMethodManager imm, final String imePackageName) {
+ mImm = imm;
+ mImePackageName = imePackageName;
+ mCachedSubtypeListWithImplicitlySelected = new HashMap<>();
+ mCachedSubtypeListOnlyExplicitlySelected = new HashMap<>();
+ }
+
+ public synchronized InputMethodInfo getInputMethodOfThisIme() {
+ if (mCachedThisImeInfo != null) {
+ return mCachedThisImeInfo;
+ }
+ for (final InputMethodInfo imi : mImm.getInputMethodList()) {
+ if (imi.getPackageName().equals(mImePackageName)) {
+ mCachedThisImeInfo = imi;
+ return imi;
+ }
+ }
+ throw new RuntimeException("Input method id for " + mImePackageName + " not found.");
+ }
+
+ public synchronized List getEnabledInputMethodSubtypeList(
+ final InputMethodInfo imi, final boolean allowsImplicitlySelectedSubtypes) {
+ final HashMap> cache =
+ allowsImplicitlySelectedSubtypes
+ ? mCachedSubtypeListWithImplicitlySelected
+ : mCachedSubtypeListOnlyExplicitlySelected;
+ final List cachedList = cache.get(imi);
+ if (cachedList != null) {
+ return cachedList;
+ }
+ final List result;
+ if (imi == getInputMethodOfThisIme()) {
+ // allowsImplicitlySelectedSubtypes means system should choose if nothing is enabled,
+ // use it to fall back to system locales or en_US to avoid returning an empty list
+ result = SubtypeSettings.INSTANCE.getEnabledSubtypes(allowsImplicitlySelectedSubtypes);
+ } else {
+ result = mImm.getEnabledInputMethodSubtypeList(imi, allowsImplicitlySelectedSubtypes);
+ }
+ cache.put(imi, result);
+ return result;
+ }
+
+ public synchronized void clear() {
+ mCachedThisImeInfo = null;
+ mCachedSubtypeListWithImplicitlySelected.clear();
+ mCachedSubtypeListOnlyExplicitlySelected.clear();
+ }
+ }
+
+ public InputMethodInfo getInputMethodInfoOfThisIme() {
+ return mInputMethodInfoCache.getInputMethodOfThisIme();
+ }
+
+ public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) {
+ return getEnabledInputMethodSubtypeList(getInputMethodInfoOfThisIme(), true)
+ .contains(subtype);
+ }
+
+ public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(final InputMethodSubtype subtype) {
+ final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
+ final boolean subtypeExplicitlyEnabled = getMyEnabledInputMethodSubtypeList(false)
+ .contains(subtype);
+ return subtypeEnabled && !subtypeExplicitlyEnabled;
+ }
+
+ public void onSubtypeChanged(@NonNull final InputMethodSubtype newSubtype) {
+ updateCurrentSubtype(newSubtype);
+ updateShortcutIme();
+ if (DEBUG) {
+ Log.w(TAG, "onSubtypeChanged: " + mCurrentRichInputMethodSubtype);
+ }
+ }
+
+ private static RichInputMethodSubtype sForcedSubtypeForTesting = null;
+
+ static void forceSubtype(@NonNull final InputMethodSubtype subtype) {
+ sForcedSubtypeForTesting = RichInputMethodSubtype.Companion.get(subtype);
+ }
+
+ @NonNull
+ public Locale getCurrentSubtypeLocale() {
+ if (null != sForcedSubtypeForTesting) {
+ return sForcedSubtypeForTesting.getLocale();
+ }
+ return getCurrentSubtype().getLocale();
+ }
+
+ @NonNull
+ public RichInputMethodSubtype getCurrentSubtype() {
+ if (null != sForcedSubtypeForTesting) {
+ return sForcedSubtypeForTesting;
+ }
+ return mCurrentRichInputMethodSubtype;
+ }
+
+
+ public String getCombiningRulesExtraValueOfCurrentSubtype() {
+ return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype());
+ }
+
+ public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) {
+ final List enabledImis = mImm.getEnabledInputMethodList();
+ return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis);
+ }
+
+ public boolean hasMultipleEnabledSubtypesInThisIme(
+ final boolean shouldIncludeAuxiliarySubtypes) {
+ final List imiList = Collections.singletonList(
+ getInputMethodInfoOfThisIme());
+ return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList);
+ }
+
+ private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes,
+ final List imiList) {
+ // Number of the filtered IMEs
+ int filteredImisCount = 0;
+
+ for (InputMethodInfo imi : imiList) {
+ // We can return true immediately after we find two or more filtered IMEs.
+ if (filteredImisCount > 1) return true;
+ final List subtypes = getEnabledInputMethodSubtypeList(imi, true);
+ // IMEs that have no subtypes should be counted.
+ if (subtypes.isEmpty()) {
+ ++filteredImisCount;
+ continue;
+ }
+
+ int auxCount = 0;
+ for (InputMethodSubtype subtype : subtypes) {
+ if (subtype.isAuxiliary()) {
+ ++auxCount;
+ }
+ }
+ final int nonAuxCount = subtypes.size() - auxCount;
+
+ // IMEs that have one or more non-auxiliary subtypes should be counted.
+ // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
+ // subtypes should be counted as well.
+ if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) {
+ ++filteredImisCount;
+ }
+ }
+
+ if (filteredImisCount > 1) {
+ return true;
+ }
+ final List subtypes = getMyEnabledInputMethodSubtypeList(true);
+ int keyboardCount = 0;
+ // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's
+ // both explicitly and implicitly enabled input method subtype.
+ // (The current IME should be LatinIME.)
+ for (InputMethodSubtype subtype : subtypes) {
+ if (KEYBOARD_MODE.equals(subtype.getMode())) {
+ ++keyboardCount;
+ }
+ }
+ return keyboardCount > 1;
+ }
+
+ public InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(final Locale locale,
+ final String keyboardLayoutSetName) {
+ final InputMethodInfo myImi = getInputMethodInfoOfThisIme();
+ final int count = myImi.getSubtypeCount();
+ for (int i = 0; i < count; i++) {
+ final InputMethodSubtype subtype = myImi.getSubtypeAt(i);
+ final String layoutName = SubtypeLocaleUtils.getMainLayoutName(subtype);
+ if (locale.equals(SubtypeUtilsKt.locale(subtype))
+ && keyboardLayoutSetName.equals(layoutName)) {
+ return subtype;
+ }
+ }
+ return null;
+ }
+
+ public InputMethodSubtype findSubtypeForHintLocale(final Locale locale) {
+ // Find the best subtype based on a locale matching
+ final List subtypes = getMyEnabledInputMethodSubtypeList(true);
+ InputMethodSubtype bestMatch = LocaleUtils.getBestMatch(locale, subtypes, SubtypeUtilsKt::locale);
+ if (bestMatch != null) return bestMatch;
+
+ // search for first secondary language & script match
+ final int count = subtypes.size();
+ final String language = locale.getLanguage();
+ final String script = ScriptUtils.script(locale);
+ for (int i = 0; i < count; ++i) {
+ final InputMethodSubtype subtype = subtypes.get(i);
+ final Locale subtypeLocale = SubtypeUtilsKt.locale(subtype);
+ if (!ScriptUtils.script(subtypeLocale).equals(script))
+ continue; // need compatible script
+ bestMatch = subtype;
+ final List secondaryLocales = SubtypeUtilsKt.getSecondaryLocales(subtype.getExtraValue());
+ for (final Locale secondaryLocale : secondaryLocales) {
+ if (secondaryLocale.getLanguage().equals(language)) {
+ return bestMatch;
+ }
+ }
+ }
+ // if wanted script is not compatible to current subtype, return a subtype with compatible script if possible
+ if (!script.equals(ScriptUtils.script(getCurrentSubtypeLocale()))) {
+ return bestMatch;
+ }
+ return null;
+ }
+
+ public List getEnabledInputMethodSubtypeList(final InputMethodInfo imi,
+ final boolean allowsImplicitlySelectedSubtypes) {
+ return mInputMethodInfoCache.getEnabledInputMethodSubtypeList(
+ imi, allowsImplicitlySelectedSubtypes);
+ }
+
+ public void refreshSubtypeCaches() {
+ mInputMethodInfoCache.clear();
+ SharedPreferences prefs = KtxKt.prefs(mContext);
+ updateCurrentSubtype(SubtypeSettings.INSTANCE.getSelectedSubtype(prefs));
+ updateShortcutIme();
+ }
+
+ private void updateCurrentSubtype(final InputMethodSubtype subtype) {
+ SubtypeSettings.INSTANCE.setSelectedSubtype(KtxKt.prefs(mContext), subtype);
+ mCurrentRichInputMethodSubtype = RichInputMethodSubtype.Companion.get(subtype);
+ }
+
+ public static boolean canSwitchLanguage() {
+ if (!isInitialized()) return false;
+ if (Settings.getValues().mLanguageSwitchKeyToOtherSubtypes && getInstance().hasMultipleEnabledSubtypesInThisIme(false))
+ return true;
+ if (Settings.getValues().mLanguageSwitchKeyToOtherImes && getInstance().mImm.getEnabledInputMethodList().size() > 1)
+ return true;
+ return false;
+ }
+
+ // todo: is shortcutIme only voice input, or can it be something else?
+ // if always voice input, rename it and other things like mHasShortcutKey
+ private void updateShortcutIme() {
+ if (DEBUG) {
+ Log.d(TAG, "Update shortcut IME from : "
+ + (mShortcutInputMethodInfo == null
+ ? "" : mShortcutInputMethodInfo.getId()) + ", "
+ + (mShortcutSubtype == null ? "" : (
+ mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
+ }
+ final RichInputMethodSubtype richSubtype = mCurrentRichInputMethodSubtype;
+ final boolean implicitlyEnabledSubtype = checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(
+ richSubtype.getRawSubtype());
+ final Locale systemLocale = ConfigurationCompatKt.locale(mContext.getResources().getConfiguration());
+ LanguageOnSpacebarUtils.onSubtypeChanged(
+ richSubtype, implicitlyEnabledSubtype, systemLocale);
+ LanguageOnSpacebarUtils.setEnabledSubtypes(getMyEnabledInputMethodSubtypeList(
+ true /* allowsImplicitlySelectedSubtypes */));
+
+ // TODO: Update an icon for shortcut IME
+ final Map> shortcuts =
+ getInputMethodManager().getShortcutInputMethodsAndSubtypes();
+ mShortcutInputMethodInfo = null;
+ mShortcutSubtype = null;
+ for (final InputMethodInfo imi : shortcuts.keySet()) {
+ final List subtypes = shortcuts.get(imi);
+ // TODO: Returns the first found IMI for now. Should handle all shortcuts as
+ // appropriate.
+ mShortcutInputMethodInfo = imi;
+ // TODO: Pick up the first found subtype for now. Should handle all subtypes
+ // as appropriate.
+ mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null;
+ break;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Update shortcut IME to : "
+ + (mShortcutInputMethodInfo == null
+ ? "" : mShortcutInputMethodInfo.getId()) + ", "
+ + (mShortcutSubtype == null ? "" : (
+ mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
+ }
+ }
+
+ public void switchToShortcutIme(final InputMethodService context) {
+ if (mShortcutInputMethodInfo == null) {
+ return;
+ }
+
+ final String imiId = mShortcutInputMethodInfo.getId();
+ switchToTargetIME(imiId, mShortcutSubtype, context);
+ }
+
+ public boolean hasShortcutIme() {
+ return mShortcutInputMethodInfo != null;
+ }
+
+ private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype,
+ final InputMethodService context) {
+ final IBinder token = context.getWindow().getWindow().getAttributes().token;
+ if (token == null) {
+ return;
+ }
+ final InputMethodManager imm = getInputMethodManager();
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ imm.setInputMethodAndSubtype(token, imiId, subtype);
+ return null;
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ public boolean isShortcutImeReady() {
+ return mShortcutInputMethodInfo != null;
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.kt b/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.kt
deleted file mode 100644
index 2e16586b5..000000000
--- a/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.kt
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- * modified
- * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
- */
-package helium314.keyboard.latin
-
-import android.content.Context
-import android.inputmethodservice.InputMethodService
-import android.os.Build
-import android.view.inputmethod.InputMethodInfo
-import android.view.inputmethod.InputMethodManager
-import android.view.inputmethod.InputMethodSubtype
-import helium314.keyboard.compat.locale
-import helium314.keyboard.latin.common.Constants
-import helium314.keyboard.latin.common.LocaleUtils.getBestMatch
-import helium314.keyboard.latin.settings.Settings
-import helium314.keyboard.latin.utils.LanguageOnSpacebarUtils
-import helium314.keyboard.latin.utils.Log
-import helium314.keyboard.latin.utils.ScriptUtils.script
-import helium314.keyboard.latin.utils.SubtypeLocaleUtils
-import helium314.keyboard.latin.utils.SubtypeSettings
-import helium314.keyboard.latin.utils.getSecondaryLocales
-import helium314.keyboard.latin.utils.locale
-import helium314.keyboard.latin.utils.prefs
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import java.util.Locale
-
-/** Enrichment class for InputMethodManager to simplify interaction and add functionality. */
-class RichInputMethodManager private constructor() {
- private lateinit var context: Context
- private lateinit var imm: InputMethodManager
- private lateinit var inputMethodInfoCache: InputMethodInfoCache
- private lateinit var currentRichInputMethodSubtype: RichInputMethodSubtype
- private var shortcutInputMethodInfo: InputMethodInfo? = null
- private var shortcutSubtype: InputMethodSubtype? = null
-
- private val isInitializedInternal get() = this::imm.isInitialized
-
- val currentSubtypeLocale get() = forcedSubtypeForTesting?.locale ?: currentSubtype.locale
-
- val currentSubtype get() = forcedSubtypeForTesting ?: currentRichInputMethodSubtype
-
- val combiningRulesExtraValueOfCurrentSubtype get() =
- SubtypeLocaleUtils.getCombiningRulesExtraValue(currentSubtype.rawSubtype)
-
- val inputMethodInfoOfThisIme get() = inputMethodInfoCache.inputMethodOfThisIme
-
- val inputMethodManager: InputMethodManager get() {
- checkInitialized()
- return imm
- }
-
- val isShortcutImeReady get() = shortcutInputMethodInfo != null
-
- fun getMyEnabledInputMethodSubtypes(allowsImplicitlySelectedSubtypes: Boolean) =
- SubtypeSettings.getEnabledSubtypes(allowsImplicitlySelectedSubtypes)
-
- fun getEnabledInputMethodSubtypes(imi: InputMethodInfo, allowsImplicitlySelectedSubtypes: Boolean) =
- inputMethodInfoCache.getEnabledInputMethodSubtypeList(imi, allowsImplicitlySelectedSubtypes)
-
- fun hasMultipleEnabledIMEsOrSubtypes(shouldIncludeAuxiliarySubtypes: Boolean) =
- hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imm.enabledInputMethodList)
-
- fun hasMultipleEnabledSubtypesInThisIme(shouldIncludeAuxiliarySubtypes: Boolean) =
- SubtypeSettings.getEnabledSubtypes(shouldIncludeAuxiliarySubtypes).size > 1
-
- fun getNextSubtypeInThisIme(onlyCurrentIme: Boolean): InputMethodSubtype? {
- val currentSubtype = currentSubtype.rawSubtype
- val enabledSubtypes = getMyEnabledInputMethodSubtypes(true)
- val currentIndex = enabledSubtypes.indexOf(currentSubtype)
- if (currentIndex == -1) {
- Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype=" +
- SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype))
- return if (onlyCurrentIme) enabledSubtypes[0] // just return first enabled subtype
- else null
- }
- val nextIndex = (currentIndex + 1) % enabledSubtypes.size
- if (nextIndex <= currentIndex && !onlyCurrentIme) {
- // The current subtype is the last or only enabled one and it needs to switch to next IME.
- return null
- }
- return enabledSubtypes[nextIndex]
- }
-
- fun findSubtypeForHintLocale(locale: Locale): InputMethodSubtype? {
- // Find the best subtype based on a locale matching
- val subtypes = getMyEnabledInputMethodSubtypes(true)
- var bestMatch = getBestMatch(locale, subtypes) { it.locale() }
- if (bestMatch != null) return bestMatch
-
- // search for first secondary language & script match
- val language = locale.language
- val script = locale.script()
- for (subtype in subtypes) {
- val subtypeLocale = subtype.locale()
- if (subtypeLocale.script() != script) continue // need compatible script
-
- bestMatch = subtype
- val secondaryLocales = getSecondaryLocales(subtype.extraValue)
- for (secondaryLocale in secondaryLocales) {
- if (secondaryLocale.language == language) {
- return bestMatch
- }
- }
- }
- // if wanted script is not compatible to current subtype, return a subtype with compatible script if available
- if (script != currentSubtypeLocale.script()) {
- return bestMatch
- }
- return null
- }
-
- fun onSubtypeChanged(newSubtype: InputMethodSubtype) {
- SubtypeSettings.setSelectedSubtype(context.prefs(), newSubtype)
- currentRichInputMethodSubtype = RichInputMethodSubtype.get(newSubtype)
- updateShortcutIme()
- if (DEBUG) {
- Log.w(TAG, "onSubtypeChanged: $currentRichInputMethodSubtype")
- }
- }
-
- fun refreshSubtypeCaches() {
- inputMethodInfoCache.clear()
- currentRichInputMethodSubtype = RichInputMethodSubtype.get(SubtypeSettings.getSelectedSubtype(context.prefs()))
- updateShortcutIme()
- }
-
- fun switchToShortcutIme(inputMethodService: InputMethodService) {
- val imiId = shortcutInputMethodInfo?.id ?: return
- val token = inputMethodService.window.window?.attributes?.token ?: return
- GlobalScope.launch {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
- inputMethodService.switchInputMethod(imiId, shortcutSubtype)
- else
- @Suppress("Deprecation") imm.setInputMethodAndSubtype(token, imiId, shortcutSubtype)
- }
- }
-
- // todo: is shortcutIme only voice input, or can it be something else?
- // if always voice input, rename it and other things like mHasShortcutKey
- private fun updateShortcutIme() {
- if (DEBUG) {
- val subtype = shortcutSubtype?.let { "${it.locale()}, ${it.mode}" } ?: ""
- Log.d(TAG, ("Update shortcut IME from: ${shortcutInputMethodInfo?.id ?: ""}, $subtype"))
- }
- val richSubtype = currentRichInputMethodSubtype
- val implicitlyEnabledSubtype = SubtypeSettings.isEnabled(richSubtype.rawSubtype)
- && !SubtypeSettings.getEnabledSubtypes(false).contains(richSubtype.rawSubtype)
- val systemLocale = context.resources.configuration.locale()
- LanguageOnSpacebarUtils.onSubtypeChanged(richSubtype, implicitlyEnabledSubtype, systemLocale)
- LanguageOnSpacebarUtils.setEnabledSubtypes(getMyEnabledInputMethodSubtypes(true))
-
- // TODO: Update an icon for shortcut IME
- val shortcuts = inputMethodManager.shortcutInputMethodsAndSubtypes
- shortcutInputMethodInfo = null
- shortcutSubtype = null
- for (imi in shortcuts.keys) {
- val subtypes = shortcuts[imi] ?: continue
- // TODO: Returns the first found IMI for now. Should handle all shortcuts as appropriate.
- shortcutInputMethodInfo = imi
- // TODO: Pick up the first found subtype for now. Should handle all subtypes as appropriate.
- shortcutSubtype = if (subtypes.size > 0) subtypes[0] else null
- break
- }
- if (DEBUG) {
- val subtype = shortcutSubtype?.let { "${it.locale()}, ${it.mode}" } ?: ""
- Log.d(TAG, ("Update shortcut IME to: ${shortcutInputMethodInfo?.id ?: ""}, $subtype"))
- }
- }
-
- private fun hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes: Boolean, imiList: List): Boolean {
- // Number of the filtered IMEs
- var filteredImisCount = 0
-
- imiList.forEach { imi ->
- // We can return true immediately after we find two or more filtered IMEs.
- if (filteredImisCount > 1) return true
- val subtypes = getEnabledInputMethodSubtypes(imi, true)
- // IMEs that have no subtypes should be counted.
- if (subtypes.isEmpty()) {
- ++filteredImisCount
- return@forEach
- }
-
- var auxCount = 0
- for (subtype in subtypes) {
- if (!subtype.isAuxiliary) {
- // IMEs that have one or more non-auxiliary subtypes should be counted.
- ++filteredImisCount
- return@forEach
- }
- ++auxCount
- }
-
- // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
- // subtypes should be counted as well.
- if (shouldIncludeAuxiliarySubtypes && auxCount > 1) {
- ++filteredImisCount
- }
- }
-
- if (filteredImisCount > 1) {
- return true
- }
- val subtypes = getMyEnabledInputMethodSubtypes(true)
- // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's
- // both explicitly and implicitly enabled input method subtype.
- // (The current IME should be LatinIME.)
- return subtypes.count { it.mode == Constants.Subtype.KEYBOARD_MODE } > 1
- }
-
- private fun checkInitialized() {
- if (!isInitializedInternal) {
- throw RuntimeException("$TAG is used before initialization")
- }
- }
-
- private fun initInternal(ctx: Context) {
- if (isInitializedInternal) {
- return
- }
- imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
- context = ctx
- inputMethodInfoCache = InputMethodInfoCache(imm, ctx.packageName)
-
- // Initialize the current input method subtype and the shortcut IME.
- refreshSubtypeCaches()
- }
-
- companion object {
- private val TAG = RichInputMethodManager::class.java.simpleName
- private const val DEBUG = false
-
- private val instance = RichInputMethodManager()
-
- @JvmStatic
- fun getInstance(): RichInputMethodManager {
- instance.checkInitialized()
- return instance
- }
-
- fun init(ctx: Context) {
- instance.initInternal(ctx)
- }
-
- @JvmStatic
- fun isInitialized() = instance.isInitializedInternal
-
- private var forcedSubtypeForTesting: RichInputMethodSubtype? = null
-
- fun forceSubtype(subtype: InputMethodSubtype) {
- forcedSubtypeForTesting = RichInputMethodSubtype.get(subtype)
- }
-
- fun canSwitchLanguage(): Boolean {
- if (!isInitialized()) return false
- if (Settings.getValues().mLanguageSwitchKeyToOtherSubtypes && instance.hasMultipleEnabledSubtypesInThisIme(false)) return true
- if (Settings.getValues().mLanguageSwitchKeyToOtherImes && instance.imm.enabledInputMethodList.size > 1) return true
- return false
- }
- }
-}
-
-private class InputMethodInfoCache(private val imm: InputMethodManager, private val imePackageName: String) {
- private var cachedThisImeInfo: InputMethodInfo? = null
- private val cachedSubtypeListWithImplicitlySelected = HashMap>()
-
- private val cachedSubtypeListOnlyExplicitlySelected = HashMap>()
-
- @get:Synchronized
- val inputMethodOfThisIme: InputMethodInfo get() {
- if (cachedThisImeInfo == null)
- cachedThisImeInfo = imm.inputMethodList.firstOrNull { it.packageName == imePackageName }
- cachedThisImeInfo?.let { return it }
- throw RuntimeException("Input method id for $imePackageName not found, only found " +
- imm.inputMethodList.map { it.packageName })
- }
-
- @Synchronized
- fun getEnabledInputMethodSubtypeList(imi: InputMethodInfo, allowsImplicitlySelectedSubtypes: Boolean): List {
- val cache = if (allowsImplicitlySelectedSubtypes) cachedSubtypeListWithImplicitlySelected
- else cachedSubtypeListOnlyExplicitlySelected
- cache[imi]?.let { return it }
- val result = if (imi == inputMethodOfThisIme) {
- // allowsImplicitlySelectedSubtypes means system should choose if nothing is enabled,
- // use it to fall back to system locales or en_US to avoid returning an empty list
- SubtypeSettings.getEnabledSubtypes(allowsImplicitlySelectedSubtypes)
- } else {
- imm.getEnabledInputMethodSubtypeList(imi, allowsImplicitlySelectedSubtypes)
- }
- cache[imi] = result
- return result
- }
-
- @Synchronized
- fun clear() {
- cachedThisImeInfo = null
- cachedSubtypeListWithImplicitlySelected.clear()
- cachedSubtypeListOnlyExplicitlySelected.clear()
- }
-}
diff --git a/app/src/main/java/helium314/keyboard/latin/RichInputMethodSubtype.kt b/app/src/main/java/helium314/keyboard/latin/RichInputMethodSubtype.kt
index 0b0ccd8b9..5764bd2d5 100644
--- a/app/src/main/java/helium314/keyboard/latin/RichInputMethodSubtype.kt
+++ b/app/src/main/java/helium314/keyboard/latin/RichInputMethodSubtype.kt
@@ -10,15 +10,12 @@ import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder
import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
+import helium314.keyboard.latin.common.LocaleUtils.isRtlLanguage
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.LayoutUtilsCustom
import helium314.keyboard.latin.utils.Log
-import helium314.keyboard.latin.utils.ScriptUtils
-import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
-import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.locale
-import helium314.keyboard.latin.utils.mainLayoutNameOrQwerty
import java.util.Locale
/**
@@ -28,7 +25,7 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
val locale: Locale = rawSubtype.locale()
// The subtype is considered RTL if the language of the main subtype is RTL.
- val isRtlSubtype: Boolean = ScriptUtils.isScriptRtl(locale.script())
+ val isRtlSubtype: Boolean = isRtlLanguage(locale)
fun getExtraValueOf(key: String): String? = rawSubtype.getExtraValueOf(key)
@@ -43,9 +40,21 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
val isCustom: Boolean get() = LayoutUtilsCustom.isCustomLayout(mainLayoutName)
- val fullDisplayName: String get() = SubtypeLocaleUtils.getSubtypeLocaleDisplayName(locale)
+ val fullDisplayName: String get() {
+ if (isNoLanguage) {
+ return SubtypeLocaleUtils.getMainLayoutDisplayName(rawSubtype)!!
+ }
+ return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(locale)
+ }
- val middleDisplayName: String get() = SubtypeLocaleUtils.getSubtypeLanguageDisplayName(locale)
+ val middleDisplayName: String
+ // Get the RichInputMethodSubtype's middle display name in its locale.
+ get() {
+ if (isNoLanguage) {
+ return SubtypeLocaleUtils.getMainLayoutDisplayName(rawSubtype)!!
+ }
+ return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(locale)
+ }
override fun equals(other: Any?): Boolean {
if (other !is RichInputMethodSubtype) return false
@@ -72,7 +81,7 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
+ "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE)
private val DUMMY_NO_LANGUAGE_SUBTYPE = RichInputMethodSubtype(
InputMethodSubtypeBuilder()
- .setSubtypeNameResId(R.string.subtype_no_language)
+ .setSubtypeNameResId(R.string.subtype_no_language_qwerty)
.setSubtypeIconResId(R.drawable.ic_ime_switcher)
.setSubtypeLocale(SubtypeLocaleUtils.NO_LANGUAGE)
.setSubtypeMode(Constants.Subtype.KEYBOARD_MODE)
@@ -106,8 +115,11 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
val noLanguageSubtype: RichInputMethodSubtype get() {
sNoLanguageSubtype?.let { return it }
var noLanguageSubtype = sNoLanguageSubtype
- val rawNoLanguageSubtype = SubtypeSettings.getResourceSubtypesForLocale(SubtypeLocaleUtils.NO_LANGUAGE.constructLocale())
- .firstOrNull { it.mainLayoutNameOrQwerty() == SubtypeLocaleUtils.QWERTY }
+ val rawNoLanguageSubtype = RichInputMethodManager.getInstance()
+ .findSubtypeByLocaleAndKeyboardLayoutSet(
+ SubtypeLocaleUtils.NO_LANGUAGE.constructLocale(),
+ SubtypeLocaleUtils.QWERTY
+ )
if (rawNoLanguageSubtype != null) {
noLanguageSubtype = RichInputMethodSubtype(rawNoLanguageSubtype)
}
@@ -120,4 +132,4 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
return DUMMY_NO_LANGUAGE_SUBTYPE
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/helium314/keyboard/latin/SingleDictionaryFacilitator.kt b/app/src/main/java/helium314/keyboard/latin/SingleDictionaryFacilitator.kt
deleted file mode 100644
index 4e2d62486..000000000
--- a/app/src/main/java/helium314/keyboard/latin/SingleDictionaryFacilitator.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-package helium314.keyboard.latin
-
-import android.content.Context
-import android.util.LruCache
-import helium314.keyboard.keyboard.Keyboard
-import helium314.keyboard.keyboard.KeyboardSwitcher
-import helium314.keyboard.latin.DictionaryFacilitator.DictionaryInitializationListener
-import helium314.keyboard.latin.common.ComposedData
-import helium314.keyboard.latin.makedict.WordProperty
-import helium314.keyboard.latin.settings.SettingsValuesForSuggestion
-import helium314.keyboard.latin.utils.SuggestionResults
-import java.util.Locale
-import java.util.concurrent.TimeUnit
-
-/** Simple DictionaryFacilitator for a single Dictionary. Has some optional special purpose functionality. */
-class SingleDictionaryFacilitator(private val dict: Dictionary) : DictionaryFacilitator {
- var suggestionLogger: SuggestionLogger? = null
-
- // this will not work from spell checker if used together with a different keyboard app
- fun getSuggestions(word: String): SuggestionResults {
- val suggestionResults = getSuggestionResults(
- ComposedData.createForWord(word),
- NgramContext.getEmptyPrevWordsContext(0),
- KeyboardSwitcher.getInstance().keyboard, // looks like actual keyboard doesn't matter (composed data doesn't contain coordinates)
- SettingsValuesForSuggestion(false, false),
- Suggest.SESSION_ID_TYPING, SuggestedWords.INPUT_STYLE_TYPING
- )
- return suggestionResults
- }
-
- override fun getSuggestionResults(
- composedData: ComposedData, ngramContext: NgramContext, keyboard: Keyboard,
- settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int, inputStyle: Int
- ): SuggestionResults {
- val suggestionResults = SuggestionResults(
- SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext,
- false
- )
- suggestionResults.addAll(
- dict.getSuggestions(composedData, ngramContext, keyboard.proximityInfo.nativeProximityInfo,
- settingsValuesForSuggestion, sessionId, 1f,
- floatArrayOf(Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL)
- )
- )
- suggestionLogger?.onNewSuggestions(suggestionResults, composedData, ngramContext, keyboard, inputStyle)
-
- return suggestionResults
- }
-
- fun getWordProperty(word: String): WordProperty? = dict.getWordProperty(word, false)
-
- // ------------ dummy functionality ----------------
-
- override fun setValidSpellingWordReadCache(cache: LruCache) {}
-
- override fun setValidSpellingWordWriteCache(cache: LruCache) {}
-
- override fun isForLocale(locale: Locale?): Boolean = locale == dict.mLocale
-
- override fun onStartInput() {}
-
- override fun onFinishInput(context: Context) {
- dict.onFinishInput()
- }
-
- override fun closeDictionaries() {
- dict.close()
- }
-
- override fun isActive(): Boolean = true
-
- override fun getMainLocale(): Locale = dict.mLocale
-
- override fun getCurrentLocale(): Locale = mainLocale
-
- override fun usesSameSettings(locales: List, contacts: Boolean, apps: Boolean, personalization: Boolean): Boolean {
- return locales.singleOrNull() == mainLocale
- }
-
- override fun resetDictionaries(context: Context, newLocale: Locale, useContactsDict: Boolean, useAppsDict: Boolean,
- usePersonalizedDicts: Boolean, forceReloadMainDictionary: Boolean, dictNamePrefix: String, listener: DictionaryInitializationListener?
- ) { }
-
- override fun hasAtLeastOneInitializedMainDictionary(): Boolean = dict.isInitialized
-
- override fun hasAtLeastOneUninitializedMainDictionary(): Boolean = !dict.isInitialized
-
- override fun waitForLoadingMainDictionaries(timeout: Long, unit: TimeUnit) {
- }
-
- override fun addToUserHistory(
- suggestion: String, wasAutoCapitalized: Boolean, ngramContext: NgramContext,
- timeStampInSeconds: Long, blockPotentiallyOffensive: Boolean
- ) {}
-
- override fun adjustConfidences(word: String, wasAutoCapitalized: Boolean) {}
-
- override fun unlearnFromUserHistory(word: String, ngramContext: NgramContext, timeStampInSeconds: Long, eventType: Int) {}
-
- override fun isValidSpellingWord(word: String): Boolean = dict.isValidWord(word)
-
- override fun isValidSuggestionWord(word: String) = isValidSpellingWord(word)
-
- override fun removeWord(word: String) {}
-
- override fun clearUserHistoryDictionary(context: Context) {}
-
- override fun localesAndConfidences(): String? = null
-
- override fun dumpDictionaryForDebug(dictName: String) {}
-
- override fun getDictionaryStats(context: Context): List = emptyList()
-
- override fun dump(context: Context) = getDictionaryStats(context).joinToString("\n")
-
- companion object {
- interface SuggestionLogger {
- /** provides input data and suggestions returned by the library */
- fun onNewSuggestions(suggestions: SuggestionResults, composedData: ComposedData,
- ngramContext: NgramContext, keyboard: Keyboard, inputStyle: Int)
- }
- }
-}
diff --git a/app/src/main/java/helium314/keyboard/latin/Suggest.kt b/app/src/main/java/helium314/keyboard/latin/Suggest.kt
index 7e95cf6e2..3172acc6a 100644
--- a/app/src/main/java/helium314/keyboard/latin/Suggest.kt
+++ b/app/src/main/java/helium314/keyboard/latin/Suggest.kt
@@ -45,20 +45,19 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
mAutoCorrectionThreshold = threshold
}
- // todo: remove when InputLogic is ready
interface OnGetSuggestedWordsCallback {
fun onGetSuggestedWords(suggestedWords: SuggestedWords?)
}
fun getSuggestedWords(wordComposer: WordComposer, ngramContext: NgramContext, keyboard: Keyboard,
settingsValuesForSuggestion: SettingsValuesForSuggestion, isCorrectionEnabled: Boolean,
- inputStyle: Int, sequenceNumber: Int): SuggestedWords {
- return if (wordComposer.isBatchMode) {
+ inputStyle: Int, sequenceNumber: Int, callback: OnGetSuggestedWordsCallback) {
+ if (wordComposer.isBatchMode) {
getSuggestedWordsForBatchInput(wordComposer, ngramContext, keyboard, settingsValuesForSuggestion,
- inputStyle, sequenceNumber)
+ inputStyle, sequenceNumber, callback)
} else {
getSuggestedWordsForNonBatchInput(wordComposer, ngramContext, keyboard, settingsValuesForSuggestion,
- inputStyle, isCorrectionEnabled, sequenceNumber)
+ inputStyle, isCorrectionEnabled, sequenceNumber, callback)
}
}
@@ -66,7 +65,7 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
// and calls the callback function with the suggestions.
private fun getSuggestedWordsForNonBatchInput(wordComposer: WordComposer, ngramContext: NgramContext, keyboard: Keyboard,
settingsValuesForSuggestion: SettingsValuesForSuggestion, inputStyleIfNotPrediction: Int,
- isCorrectionEnabled: Boolean, sequenceNumber: Int): SuggestedWords {
+ isCorrectionEnabled: Boolean, sequenceNumber: Int, callback: OnGetSuggestedWordsCallback) {
val typedWordString = wordComposer.typedWord
val resultsArePredictions = !wordComposer.isComposingWord
val suggestionResults = if (typedWordString.isEmpty())
@@ -132,8 +131,8 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
}
}
val isTypedWordValid = firstOccurrenceOfTypedWordInSuggestions > -1 || (!resultsArePredictions && !allowsToBeAutoCorrected)
- return SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions,
- typedWordInfo, isTypedWordValid, hasAutoCorrection, false, inputStyle, sequenceNumber)
+ callback.onGetSuggestedWords(SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions,
+ typedWordInfo, isTypedWordValid, hasAutoCorrection, false, inputStyle, sequenceNumber))
}
// returns [allowsToBeAutoCorrected, hasAutoCorrection]
@@ -253,8 +252,9 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
wordComposer: WordComposer,
ngramContext: NgramContext, keyboard: Keyboard,
settingsValuesForSuggestion: SettingsValuesForSuggestion,
- inputStyle: Int, sequenceNumber: Int
- ): SuggestedWords {
+ inputStyle: Int, sequenceNumber: Int,
+ callback: OnGetSuggestedWordsCallback
+ ) {
val suggestionResults = mDictionaryFacilitator.getSuggestionResults(
wordComposer.composedDataSnapshot, ngramContext, keyboard,
settingsValuesForSuggestion, SESSION_ID_GESTURE, inputStyle
@@ -312,8 +312,10 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
} else {
suggestionsContainer
}
- return SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions, pseudoTypedWordInfo, true,
- false, false, inputStyle, sequenceNumber)
+ callback.onGetSuggestedWords(
+ SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions, pseudoTypedWordInfo, true,
+ false, false, inputStyle, sequenceNumber)
+ )
}
/** reduces score of the first suggestion if next one is close and has more than a single letter */
diff --git a/app/src/main/java/helium314/keyboard/latin/UserBinaryDictionary.java b/app/src/main/java/helium314/keyboard/latin/UserBinaryDictionary.java
index 91d963cf8..0ff78fd6e 100644
--- a/app/src/main/java/helium314/keyboard/latin/UserBinaryDictionary.java
+++ b/app/src/main/java/helium314/keyboard/latin/UserBinaryDictionary.java
@@ -82,7 +82,8 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
}
public static UserBinaryDictionary getDictionary(
- final Context context, final Locale locale, final File dictFile, final String dictNamePrefix) {
+ final Context context, final Locale locale, final File dictFile,
+ final String dictNamePrefix, @Nullable final String account) {
return new UserBinaryDictionary(context, locale, false, dictFile, dictNamePrefix + NAME);
}
diff --git a/app/src/main/java/helium314/keyboard/latin/WordComposer.java b/app/src/main/java/helium314/keyboard/latin/WordComposer.java
index 6c84ada65..203eb205c 100644
--- a/app/src/main/java/helium314/keyboard/latin/WordComposer.java
+++ b/app/src/main/java/helium314/keyboard/latin/WordComposer.java
@@ -10,6 +10,8 @@ import androidx.annotation.NonNull;
import helium314.keyboard.event.CombinerChain;
import helium314.keyboard.event.Event;
+import helium314.keyboard.keyboard.Keyboard;
+import helium314.keyboard.keyboard.KeyboardSwitcher;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
@@ -71,7 +73,7 @@ public final class WordComposer {
private boolean mIsOnlyFirstCharCapitalized;
public WordComposer() {
- mCombinerChain = new CombinerChain("", "");
+ mCombinerChain = new CombinerChain("");
mEvents = new ArrayList<>();
mAutoCorrection = null;
mIsResumed = false;
@@ -79,6 +81,11 @@ public final class WordComposer {
mCursorPositionWithinWord = 0;
mRejectedBatchModeSuggestion = null;
refreshTypedWordCache();
+ final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard();
+ if (keyboard != null)
+ // initializing with the right state is important for the spell checker,
+ // which creates a new WordComposer when receiving suggestions
+ mCombinerChain.setHangul(keyboard.mId.mSubtype.getLocale().getLanguage().equals("ko"));
}
public ComposedData getComposedDataSnapshot() {
@@ -92,11 +99,14 @@ public final class WordComposer {
public void restartCombining(final String combiningSpec) {
final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec;
if (!nonNullCombiningSpec.equals(mCombiningSpec)) {
- mCombinerChain = new CombinerChain(mCombinerChain.getComposingWordWithCombiningFeedback().toString(), nonNullCombiningSpec);
+ mCombinerChain = new CombinerChain(mCombinerChain.getComposingWordWithCombiningFeedback().toString());
mCombiningSpec = nonNullCombiningSpec;
}
}
+ /** Forwards the state to CombinerChain, which disables or enables the Hangul combiner */
+ public void setHangul(final boolean enabled) { mCombinerChain.setHangul(enabled); }
+
/**
* Clear out the keys registered so far.
*/
diff --git a/app/src/main/java/helium314/keyboard/latin/common/Colors.kt b/app/src/main/java/helium314/keyboard/latin/common/Colors.kt
index 708122828..f8a57a476 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/Colors.kt
+++ b/app/src/main/java/helium314/keyboard/latin/common/Colors.kt
@@ -278,7 +278,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override
TOOL_BAR_EXPAND_KEY_BACKGROUND -> if (!isNight) accent else doubleAdjustedBackground
GESTURE_TRAIL -> gesture
KEY_TEXT, SUGGESTION_AUTO_CORRECT, REMOVE_SUGGESTION_ICON, EMOJI_KEY_TEXT, KEY_PREVIEW_TEXT, POPUP_KEY_TEXT,
- KEY_ICON, POPUP_KEY_ICON, ONE_HANDED_MODE_BUTTON, EMOJI_CATEGORY, TOOL_BAR_KEY, FUNCTIONAL_KEY_TEXT -> keyText
+ KEY_ICON, ONE_HANDED_MODE_BUTTON, EMOJI_CATEGORY, TOOL_BAR_KEY, FUNCTIONAL_KEY_TEXT -> keyText
KEY_HINT_TEXT -> keyHintText
SPACE_BAR_TEXT -> spaceBarText
FUNCTIONAL_KEY_BACKGROUND -> functionalKey
@@ -476,7 +476,7 @@ class DefaultColors (
TOOL_BAR_EXPAND_KEY_BACKGROUND, CLIPBOARD_SUGGESTION_BACKGROUND -> doubleAdjustedBackground
GESTURE_TRAIL -> gesture
KEY_TEXT, REMOVE_SUGGESTION_ICON, FUNCTIONAL_KEY_TEXT, KEY_ICON, EMOJI_KEY_TEXT,
- POPUP_KEY_TEXT, POPUP_KEY_ICON, KEY_PREVIEW_TEXT -> keyText
+ POPUP_KEY_TEXT, KEY_PREVIEW_TEXT -> keyText
KEY_HINT_TEXT -> keyHintText
SPACE_BAR_TEXT -> spaceBarText
FUNCTIONAL_KEY_BACKGROUND -> functionalKey
@@ -637,7 +637,6 @@ enum class ColorType {
MORE_SUGGESTIONS_WORD_BACKGROUND,
POPUP_KEYS_BACKGROUND,
POPUP_KEY_TEXT,
- POPUP_KEY_ICON,
NAVIGATION_BAR,
SHIFT_KEY_ICON,
SPACE_BAR_BACKGROUND,
diff --git a/app/src/main/java/helium314/keyboard/latin/common/ComposedData.java b/app/src/main/java/helium314/keyboard/latin/common/ComposedData.java
new file mode 100644
index 000000000..7e42f380a
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/common/ComposedData.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.latin.common;
+
+import androidx.annotation.NonNull;
+
+/**
+ * An immutable class that encapsulates a snapshot of word composition data.
+ */
+public class ComposedData {
+ @NonNull
+ public final InputPointers mInputPointers;
+ public final boolean mIsBatchMode;
+ @NonNull
+ public final String mTypedWord;
+
+ public ComposedData(@NonNull final InputPointers inputPointers, final boolean isBatchMode,
+ @NonNull final String typedWord) {
+ mInputPointers = inputPointers;
+ mIsBatchMode = isBatchMode;
+ mTypedWord = typedWord;
+ }
+
+ /**
+ * Copy the code points in the typed word to a destination array of ints.
+ *
+ * If the array is too small to hold the code points in the typed word, nothing is copied and
+ * -1 is returned.
+ *
+ * @param destination the array of ints.
+ * @return the number of copied code points.
+ */
+ public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
+ @NonNull final int[] destination) {
+ // lastIndex is exclusive
+ final int lastIndex = mTypedWord.length()
+ - StringUtils.getTrailingSingleQuotesCount(mTypedWord);
+ if (lastIndex <= 0) {
+ // The string is empty or contains only single quotes.
+ return 0;
+ }
+
+ // The following function counts the number of code points in the text range which begins
+ // at index 0 and extends to the character at lastIndex.
+ final int codePointSize = Character.codePointCount(mTypedWord, 0, lastIndex);
+ if (codePointSize > destination.length) {
+ return -1;
+ }
+ return StringUtils.copyCodePointsAndReturnCodePointCount(destination, mTypedWord, 0,
+ lastIndex, true /* downCase */);
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/common/ComposedData.kt b/app/src/main/java/helium314/keyboard/latin/common/ComposedData.kt
deleted file mode 100644
index 091c41920..000000000
--- a/app/src/main/java/helium314/keyboard/latin/common/ComposedData.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- * modified
- * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
- */
-package helium314.keyboard.latin.common
-
-import helium314.keyboard.latin.WordComposer
-import kotlin.random.Random
-
-/** An immutable class that encapsulates a snapshot of word composition data. */
-class ComposedData(
- @JvmField val mInputPointers: InputPointers,
- @JvmField val mIsBatchMode: Boolean,
- @JvmField val mTypedWord: String
-) {
- /**
- * Copy the code points in the typed word to a destination array of ints.
- *
- * If the array is too small to hold the code points in the typed word, nothing is copied and
- * -1 is returned.
- *
- * @param destination the array of ints.
- * @return the number of copied code points.
- */
- fun copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
- destination: IntArray
- ): Int {
- // lastIndex is exclusive
- val lastIndex = (mTypedWord.length - StringUtils.getTrailingSingleQuotesCount(mTypedWord))
- if (lastIndex <= 0) {
- return 0 // The string is empty or contains only single quotes.
- }
-
- // The following function counts the number of code points in the text range which begins
- // at index 0 and extends to the character at lastIndex.
- val codePointSize = Character.codePointCount(mTypedWord, 0, lastIndex)
- if (codePointSize > destination.size) {
- return -1
- }
- return StringUtils.copyCodePointsAndReturnCodePointCount(
- destination, mTypedWord, 0, lastIndex, true
- )
- }
-
- companion object {
- fun createForWord(word: String): ComposedData {
- val codePoints = StringUtils.toCodePointArray(word)
- val coordinates = CoordinateUtils.newCoordinateArray(codePoints.size)
- for (i in codePoints.indices) {
- CoordinateUtils.setXYInArray(coordinates, i, Random.nextBits(2), Random.nextBits(2))
- }
- return WordComposer().apply { setComposingWord(codePoints, coordinates) }.composedDataSnapshot
- }
- }
-}
diff --git a/app/src/main/java/helium314/keyboard/latin/common/Constants.java b/app/src/main/java/helium314/keyboard/latin/common/Constants.java
index b61586bb2..4dedec0ec 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/Constants.java
+++ b/app/src/main/java/helium314/keyboard/latin/common/Constants.java
@@ -77,7 +77,7 @@ public final class Constants {
/** Indicates that this subtype is an additional subtype that the user defined. This extra value is private to LatinIME. */
public static final String IS_ADDITIONAL_SUBTYPE = "isAdditionalSubtype";
- /** The subtype extra value used to specify the combining rules. */
+ /** The subtype extra value used to specify the combining rules (currently not used). */
public static final String COMBINING_RULES = "CombiningRules";
/** Overrides the general popup order setting */
@@ -197,8 +197,6 @@ public final class Constants {
public static final int CODE_GRAVE_ACCENT = '`';
public static final int CODE_CIRCUMFLEX_ACCENT = '^';
public static final int CODE_TILDE = '~';
- public static final int RECENTS_TEMPLATE_KEY_CODE_0 = 0x30;
- public static final int RECENTS_TEMPLATE_KEY_CODE_1 = 0x31;
public static final String REGEXP_PERIOD = "\\.";
public static final String STRING_SPACE = " ";
diff --git a/app/src/main/java/helium314/keyboard/latin/common/Constants.kt b/app/src/main/java/helium314/keyboard/latin/common/Constants.kt
index 43b9e66dd..17b36d1da 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/Constants.kt
+++ b/app/src/main/java/helium314/keyboard/latin/common/Constants.kt
@@ -12,5 +12,3 @@ object Links {
const val CUSTOM_LAYOUTS = "$GITHUB/discussions/categories/custom-layout"
const val CUSTOM_COLORS = "$GITHUB/discussions/categories/custom-colors"
}
-
-val combiningRange = 0x300..0x35b
diff --git a/app/src/main/java/helium314/keyboard/latin/common/LocaleUtils.kt b/app/src/main/java/helium314/keyboard/latin/common/LocaleUtils.kt
index f06ca7332..377d0c08d 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/LocaleUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/common/LocaleUtils.kt
@@ -8,10 +8,10 @@ package helium314.keyboard.latin.common
import android.content.Context
import android.content.res.Resources
import helium314.keyboard.compat.locale
+import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.R
import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
-import helium314.keyboard.latin.utils.runInLocale
import java.util.Locale
/**
@@ -171,32 +171,34 @@ object LocaleUtils {
}
}
- fun Locale.localizedDisplayName(resources: Resources, displayLocale: Locale? = null): String {
- val languageTag = toLanguageTag()
- if (languageTag == SubtypeLocaleUtils.NO_LANGUAGE)
- return resources.getString(R.string.subtype_no_language)
-
- val overrideResId = when (languageTag) {
- "en-US" -> R.string.subtype_en_US
- "en-GB" -> R.string.subtype_en_GB
- "es-US" -> R.string.subtype_es_US
- "hi-Latn" -> R.string.subtype_hi_Latn
- "sr-Latn" -> R.string.subtype_sr_Latn
- "mns" -> R.string.subtype_mns
- "xdq" -> R.string.subtype_xdq
- "dru" -> R.string.subtype_xdq
- "st" -> R.string.subtype_st
- "dag" -> R.string.subtype_dag
- else -> 0
- }
- if (overrideResId != 0) {
- return if (displayLocale == null) resources.getString(overrideResId)
- else runInLocale(resources, displayLocale) { it.getString(overrideResId) }
+ @JvmStatic
+ fun isRtlLanguage(locale: Locale): Boolean {
+ val displayName = locale.getDisplayName(locale)
+ if (displayName.isEmpty()) return false
+ return when (Character.getDirectionality(displayName.codePointAt(0))) {
+ Character.DIRECTIONALITY_RIGHT_TO_LEFT, Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC -> true
+ else -> false
}
+ }
- val localeDisplayName = getDisplayName(displayLocale ?: resources.configuration.locale())
+ fun Locale.localizedDisplayName(context: Context) =
+ getLocaleDisplayNameInLocale(this, context.resources, context.resources.configuration.locale())
+
+ @JvmStatic
+ fun getLocaleDisplayNameInLocale(locale: Locale, resources: Resources, displayLocale: Locale): String {
+ val languageTag = locale.toLanguageTag()
+ if (languageTag == SubtypeLocaleUtils.NO_LANGUAGE) return resources.getString(R.string.subtype_no_language)
+ if (locale.script() != locale.language.constructLocale().script() || locale.language == "mns" || locale.language == "xdq" || locale.language=="dru") {
+ val resId = resources.getIdentifier(
+ "subtype_${languageTag.replace("-", "_")}",
+ "string",
+ BuildConfig.APPLICATION_ID // replaces context.packageName, see https://stackoverflow.com/a/24525379
+ )
+ if (resId != 0) return resources.getString(resId)
+ }
+ val localeDisplayName = locale.getDisplayName(displayLocale)
return if (localeDisplayName == languageTag) {
- getDisplayName(Locale.US) // try fallback to English name, relevant e.g. fpr pms, see https://github.com/Helium314/HeliBoard/pull/748
+ locale.getDisplayName(Locale.US) // try fallback to English name, relevant e.g. fpr pms, see https://github.com/Helium314/HeliBoard/pull/748
} else {
localeDisplayName
}
diff --git a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt
index 5a501aaad..5b8af87c3 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt
@@ -6,72 +6,67 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.common.StringUtils.mightBeEmoji
import helium314.keyboard.latin.common.StringUtils.newSingleCodePointString
import helium314.keyboard.latin.settings.SpacingAndPunctuations
-import helium314.keyboard.latin.utils.ScriptUtils
-import helium314.keyboard.latin.utils.SpacedTokens
-import helium314.keyboard.latin.utils.SpannableStringUtils
-import helium314.keyboard.latin.utils.TextRange
import java.math.BigInteger
import java.util.Locale
-import kotlin.math.max
-fun CharSequence.codePointAt(offset: Int) = Character.codePointAt(this, offset)
-fun CharSequence.codePointBefore(offset: Int) = Character.codePointBefore(this, offset)
-
-/** Loops over the codepoints in [text]. Exits when [loop] returns true */
-inline fun loopOverCodePoints(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
+fun loopOverCodePoints(s: CharSequence, run: (Int) -> Boolean) {
+ val text = if (s is String) s else s.toString()
var offset = 0
while (offset < text.length) {
- val cp = text.codePointAt(offset)
- val charCount = Character.charCount(cp)
- if (loop(cp, charCount)) return
- offset += charCount
+ val codepoint = text.codePointAt(offset)
+ if (run(codepoint)) return
+ offset += Character.charCount(codepoint)
}
}
-/** Loops backwards over the codepoints in [text]. Exits when [loop] returns true */
-inline fun loopOverCodePointsBackwards(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
+fun loopOverCodePointsBackwards(s: CharSequence, run: (Int) -> Boolean) {
+ val text = if (s is String) s else s.toString()
var offset = text.length
while (offset > 0) {
- val cp = text.codePointBefore(offset)
- val charCount = Character.charCount(cp)
- if (loop(cp, charCount)) return
- offset -= charCount
+ val codepoint = text.codePointBefore(offset)
+ if (run(codepoint)) return
+ offset -= Character.charCount(codepoint)
}
}
-fun nonWordCodePointAndNoSpaceBeforeCursor(text: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
+fun nonWordCodePointAndNoSpaceBeforeCursor(s: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
var space = false
var nonWordCodePoint = false
- loopOverCodePointsBackwards(text) { cp, _ ->
- if (!space && Character.isWhitespace(cp)) space = true
- // treat double quote like a word codepoint for this function (not great, maybe clarify name or extend list of chars?)
- if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(cp) && cp != '"'.code) {
+ loopOverCodePointsBackwards(s) {
+ if (!space && Character.isWhitespace(it))
+ space = true
+ // treat double quote like a word codepoint for the purpose of this function (not great, maybe clarify name, or extend list of chars?)
+ if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(it) && it != '"'.code)
nonWordCodePoint = true
- }
space && nonWordCodePoint // stop if both are found
}
- return nonWordCodePoint && !space // return true if a non-word codepoint and no space was found
+ return nonWordCodePoint && !space // return true if an non-word codepoint and no space was found
}
-fun hasLetterBeforeLastSpaceBeforeCursor(text: CharSequence): Boolean {
- loopOverCodePointsBackwards(text) { cp, _ ->
- if (Character.isWhitespace(cp)) return false
- else if (Character.isLetter(cp)) return true
- false // continue
+fun hasLetterBeforeLastSpaceBeforeCursor(s: CharSequence): Boolean {
+ var letter = false
+ loopOverCodePointsBackwards(s) {
+ if (Character.isWhitespace(it)) true
+ else if (Character.isLetter(it)) {
+ letter = true
+ true
+ }
+ else false
}
- return false
+ return letter
}
-/** get the complete emoji at end of [text], considering that emojis can be joined with ZWJ resulting in different emojis */
-fun getFullEmojiAtEnd(text: CharSequence): String {
- val s = text.toString()
- var offset = s.length
+/** get the complete emoji at end of [s], considering that emojis can be joined with ZWJ resulting in different emojis */
+fun getFullEmojiAtEnd(s: CharSequence): String {
+ val text = if (s is String) s else s.toString()
+ var offset = text.length
while (offset > 0) {
- val codepoint = s.codePointBefore(offset)
+ val codepoint = text.codePointBefore(offset)
// stop if codepoint can't be emoji
- if (!mightBeEmoji(codepoint)) return text.substring(offset)
+ if (!mightBeEmoji(codepoint))
+ return text.substring(offset)
offset -= Character.charCount(codepoint)
- if (offset > 0 && s[offset - 1].code == KeyCode.ZWJ) {
+ if (offset > 0 && text[offset - 1].code == KeyCode.ZWJ) {
// todo: this appends ZWJ in weird cases like text, ZWJ, emoji
// and detects single ZWJ as emoji (at least irrelevant for current use of getFullEmojiAtEnd)
offset -= 1
@@ -81,124 +76,21 @@ fun getFullEmojiAtEnd(text: CharSequence): String {
if (codepoint in 0x1F3FB..0x1F3FF) {
// Skin tones are not added with ZWJ, but just appended. This is not nice as they can be emojis on their own,
// but that's how it is done. Assume that an emoji before the skin tone will get merged (usually correct in practice)
- val codepointBefore = s.codePointBefore(offset)
+ val codepointBefore = text.codePointBefore(offset)
if (isEmoji(codepointBefore)) {
offset -= Character.charCount(codepointBefore)
continue
}
}
// check the whole text after offset
- val textToCheck = s.substring(offset)
- if (isEmoji(textToCheck)) return textToCheck
- }
- return s.substring(offset)
-}
-
-/**
- * Returns whether the [text] does not end with word separator, ignoring all word connectors.
- * If the [text] is empty (after ignoring word connectors), the method returns false.
- */
-// todo: this returns true on numbers, why isn't Character.isLetter(code) used?
-fun endsWithWordCodepoint(text: String, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
- if (text.isEmpty()) return false
- var codePoint = 0 // initial value irrelevant since length is always > 0
- loopOverCodePointsBackwards(text) { cp, _ ->
- codePoint = cp
- !spacingAndPunctuations.isWordConnector(cp)
- }
- // codePoint might still be a wordConnector (if text consists of wordConnectors)
- return !spacingAndPunctuations.isWordConnector(codePoint) && !spacingAndPunctuations.isWordSeparator(codePoint)
-}
-
-// todo: simplify... maybe compare with original code?
-fun getTouchedWordRange(before: CharSequence, after: CharSequence, script: String, spacingAndPunctuations: SpacingAndPunctuations): TextRange {
- // Going backward, find the first breaking point (separator)
- var startIndexInBefore = before.length
- var endIndexInAfter = -1 // todo: clarify why might we want to set it when checking before
- loopOverCodePointsBackwards(before) { codePoint, cpLength ->
- if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
- if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
- return@loopOverCodePointsBackwards true
- // continue to the next whitespace and see whether this contains a sometimesWordConnector
- for (i in startIndexInBefore - 1 downTo 0) {
- val c = before[i]
- if (spacingAndPunctuations.isSometimesWordConnector(c.code)) {
- // if yes -> whitespace is the index
- startIndexInBefore = max(StringUtils.charIndexOfLastWhitespace(before).toDouble(), 0.0).toInt()
- val firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after)
- endIndexInAfter = if (firstSpaceAfter == -1) after.length else firstSpaceAfter - 1
- return@loopOverCodePointsBackwards true
- } else if (Character.isWhitespace(c)) {
- // if no, just break normally
- return@loopOverCodePointsBackwards true
- }
- }
- return@loopOverCodePointsBackwards true
- }
- startIndexInBefore -= cpLength
- false
- }
-
- // Find last word separator after the cursor
- if (endIndexInAfter == -1) {
- endIndexInAfter = 0
- loopOverCodePoints(after) { codePoint, cpLength ->
- if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
- if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
- return@loopOverCodePoints true
- // continue to the next whitespace and see whether this contains a sometimesWordConnector
- for (i in endIndexInAfter.. whitespace is next to the index
- startIndexInBefore = max(StringUtils.charIndexOfLastWhitespace(before), 0)
- val firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after)
- endIndexInAfter = if (firstSpaceAfter == -1) after.length else firstSpaceAfter - 1
- return@loopOverCodePoints true
- } else if (Character.isWhitespace(c)) {
- // if no, just break normally
- return@loopOverCodePoints true
- }
- }
- return@loopOverCodePoints true
- }
- endIndexInAfter += cpLength
- false
+ val textToCheck = text.substring(offset)
+ if (isEmoji(textToCheck)) {
+ return textToCheck
}
}
-
- // strip text before "//" (i.e. ignore http and other protocols)
- val beforeConsideringStart = before.substring(startIndexInBefore, before.length)
- val protocolEnd = beforeConsideringStart.lastIndexOf("//")
- if (protocolEnd != -1) startIndexInBefore += protocolEnd + 1
-
- // we don't want the end characters to be word separators
- while (endIndexInAfter > 0 && spacingAndPunctuations.isWordSeparator(after[endIndexInAfter - 1].code)) {
- --endIndexInAfter
- }
- while (startIndexInBefore < before.length && spacingAndPunctuations.isWordSeparator(before[startIndexInBefore].code)) {
- ++startIndexInBefore
- }
-
- val hasUrlSpans = SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length)
- || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter)
-
- // We don't use TextUtils#concat because it copies all spans without respect to their
- // nature. If the text includes a PARAGRAPH span and it has been split, then
- // TextUtils#concat will crash when it tries to concat both sides of it.
- return TextRange(
- SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after),
- startIndexInBefore, before.length + endIndexInAfter, before.length,
- hasUrlSpans
- )
+ return text.substring(offset)
}
-// actually this should not be in STRING Utils, but only used for getTouchedWordRange
-private fun isPartOfCompositionForScript(codePoint: Int, spacingAndPunctuations: SpacingAndPunctuations, script: String) =
- spacingAndPunctuations.isWordConnector(codePoint) // We always consider word connectors part of compositions.
- // Otherwise, it's part of composition if it's part of script and not a separator.
- || (!spacingAndPunctuations.isWordSeparator(codePoint) && ScriptUtils.isLetterPartOfScript(codePoint, script))
-
/** split the string on the first of consecutive space only, further consecutive spaces are added to the next split */
fun String.splitOnFirstSpacesOnly(): List {
val out = mutableListOf()
@@ -218,7 +110,8 @@ fun String.splitOnFirstSpacesOnly(): List {
sb.append(c)
}
}
- if (sb.isNotBlank()) out.add(sb.toString())
+ if (sb.isNotBlank())
+ out.add(sb.toString())
return out
}
@@ -227,7 +120,8 @@ fun CharSequence.isValidNumber(): Boolean {
}
fun String.decapitalize(locale: Locale): String {
- if (isEmpty() || !this[0].isUpperCase()) return this
+ if (isEmpty() || !this[0].isUpperCase())
+ return this
return replaceFirstChar { it.lowercase(locale) }
}
@@ -242,9 +136,11 @@ fun containsValueWhenSplit(string: String?, value: String, split: String): Boole
fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c))
-fun isEmoji(text: CharSequence): Boolean = mightBeEmoji(text) && text.matches(emoRegex)
+fun isEmoji(s: CharSequence): Boolean = mightBeEmoji(s) && s.matches(emoRegex)
-fun String.splitOnWhitespace() = SpacedTokens(this).toList()
+fun String.splitOnWhitespace() = split(whitespaceSplitRegex)
+
+private val whitespaceSplitRegex = "\\s+".toRegex()
// from https://github.com/mathiasbynens/emoji-test-regex-pattern, MIT license
// matches single emojis only
diff --git a/app/src/main/java/helium314/keyboard/latin/define/DebugFlags.kt b/app/src/main/java/helium314/keyboard/latin/define/DebugFlags.kt
index b8af61f47..6829f71e3 100644
--- a/app/src/main/java/helium314/keyboard/latin/define/DebugFlags.kt
+++ b/app/src/main/java/helium314/keyboard/latin/define/DebugFlags.kt
@@ -11,7 +11,6 @@ import android.os.Build
import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.settings.DebugSettings
import helium314.keyboard.latin.settings.Defaults
-import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.prefs
import java.io.File
@@ -28,7 +27,8 @@ object DebugFlags {
fun init(context: Context) {
DEBUG_ENABLED = context.prefs().getBoolean(DebugSettings.PREF_DEBUG_MODE, Defaults.PREF_DEBUG_MODE)
- CrashReportExceptionHandler(context.applicationContext).install()
+ if (DEBUG_ENABLED || BuildConfig.DEBUG)
+ CrashReportExceptionHandler(context.applicationContext).install()
}
}
@@ -64,17 +64,11 @@ ${Log.getLog(100).joinToString("\n")}
private fun writeCrashReportToFile(text: String) {
try {
- val dir = appContext.getExternalFilesDir(null)
+ val dir = appContext.getExternalFilesDir(null) ?: return
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(Calendar.getInstance().time)
val crashReportFile = File(dir, "crash_report_$date.txt")
- crashReportFile.appendText(text)
- } catch (_: Exception) {
- // can't write in external files dir, maybe device just booted and is still locked
- // in this case there shouldn't be any sensitive data and we can put crash logs in unprotected files dir
- val dir = DeviceProtectedUtils.getFilesDir(appContext) ?: return
- val date = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(Calendar.getInstance().time)
- val crashReportFile = File(dir, "crash_report_unprotected_$date.txt")
- crashReportFile.appendText(text)
+ crashReportFile.writeText(text)
+ } catch (ignored: IOException) {
}
}
}
diff --git a/app/src/main/java/helium314/keyboard/latin/define/ProductionFlags.kt b/app/src/main/java/helium314/keyboard/latin/define/ProductionFlags.kt
index 9aeb7cc3d..d476a0426 100644
--- a/app/src/main/java/helium314/keyboard/latin/define/ProductionFlags.kt
+++ b/app/src/main/java/helium314/keyboard/latin/define/ProductionFlags.kt
@@ -7,11 +7,15 @@
package helium314.keyboard.latin.define
object ProductionFlags {
- const val IS_HARDWARE_KEYBOARD_SUPPORTED = true
+ const val IS_HARDWARE_KEYBOARD_SUPPORTED = false
+ // todo: make it work
+ // was set to true in hangul branch (and there is the hangul hardware event decoder in latinIme)
+ // but disabled again because this breaks ctrl+c / ctrl+v, and most likely other things
+ // so it looks like the HardwareKeyboardEventDecoder needs some work before it's ready
/**
* Include all suggestions from all dictionaries in
* [helium314.keyboard.latin.SuggestedWords.mRawSuggestions].
*/
const val INCLUDE_RAW_SUGGESTIONS = false
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
index 28572fa2f..79464dee3 100644
--- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
+++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
@@ -22,6 +22,7 @@ import android.view.inputmethod.EditorInfo;
import androidx.annotation.NonNull;
import helium314.keyboard.event.Event;
+import helium314.keyboard.event.HangulEventDecoder;
import helium314.keyboard.event.InputTransaction;
import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.keyboard.KeyboardSwitcher;
@@ -43,13 +44,11 @@ import helium314.keyboard.latin.common.StringUtils;
import helium314.keyboard.latin.common.StringUtilsKt;
import helium314.keyboard.latin.common.SuggestionSpanUtilsKt;
import helium314.keyboard.latin.define.DebugFlags;
-import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.settings.SettingsValues;
import helium314.keyboard.latin.settings.SpacingAndPunctuations;
import helium314.keyboard.latin.suggestions.SuggestionStripViewAccessor;
import helium314.keyboard.latin.utils.AsyncResultHolder;
import helium314.keyboard.latin.utils.InputTypeUtils;
-import helium314.keyboard.latin.utils.IntentUtils;
import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.utils.RecapitalizeStatus;
import helium314.keyboard.latin.utils.ScriptUtils;
@@ -72,7 +71,7 @@ public final class InputLogic {
final LatinIME mLatinIME;
private final SuggestionStripViewAccessor mSuggestionStripViewAccessor;
- @NonNull private final InputLogicHandler mInputLogicHandler;
+ @NonNull private InputLogicHandler mInputLogicHandler;
// TODO : make all these fields private as soon as possible.
// Current space state of the input method. This can be any of the above constants.
@@ -90,7 +89,6 @@ public final class InputLogic {
private int mDeleteCount;
private long mLastKeyTime;
- // todo: this is not used, so either remove it or do something with it
public final TreeSet mCurrentlyPressedHardwareKeys = new TreeSet<>();
// Keeps track of most recently inserted text (multi-character key) for reverting
@@ -121,7 +119,7 @@ public final class InputLogic {
mSuggestionStripViewAccessor = suggestionStripViewAccessor;
mWordComposer = new WordComposer();
mConnection = new RichInputConnection(latinIME);
- mInputLogicHandler = new InputLogicHandler(mLatinIME.mHandler, this);
+ mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
mSuggest = new Suggest(dictionaryFacilitator);
mDictionaryFacilitator = dictionaryFacilitator;
}
@@ -131,7 +129,7 @@ public final class InputLogic {
*
* Call this when input starts or restarts in some editor (typically, in onStartInputView).
*
- * @param combiningSpec the combining spec string for this subtype (from extra value)
+ * @param combiningSpec the combining spec string for this subtype
* @param settingsValues the current settings values
*/
public void startInput(final String combiningSpec, final SettingsValues settingsValues) {
@@ -155,7 +153,11 @@ public final class InputLogic {
// editorInfo.initialSelStart is not the actual cursor position, so we try using some heuristics to find the correct position.
mConnection.tryFixIncorrectCursorPosition();
cancelDoubleSpacePeriodCountdown();
- mInputLogicHandler.reset();
+ if (InputLogicHandler.NULL_HANDLER == mInputLogicHandler) {
+ mInputLogicHandler = new InputLogicHandler(mLatinIME, this);
+ } else {
+ mInputLogicHandler.reset();
+ }
mConnection.requestCursorUpdates(true, true);
}
@@ -198,6 +200,17 @@ public final class InputLogic {
mInputLogicHandler.reset();
}
+ // Normally this class just gets out of scope after the process ends, but in unit tests, we
+ // create several instances of LatinIME in the same process, which results in several
+ // instances of InputLogic. This cleans up the associated handler so that tests don't leak
+ // handlers.
+ public void recycle() {
+ final InputLogicHandler inputLogicHandler = mInputLogicHandler;
+ mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
+ inputLogicHandler.destroy();
+ mDictionaryFacilitator.closeDictionaries();
+ }
+
/**
* React to a string input.
*
@@ -401,11 +414,7 @@ public final class InputLogic {
// Stop the last recapitalization, if started.
mRecapitalizeStatus.stop();
mWordBeingCorrectedByCursor = null;
-
- // we do not return true if
- final boolean oneSidedSelectionMove = hasOrHadSelection
- && ((oldSelEnd == newSelEnd && oldSelStart != newSelStart) || (oldSelEnd != newSelEnd && oldSelStart == newSelStart));
- return !oneSidedSelectionMove;
+ return true;
}
public boolean moveCursorByAndReturnIfInsideComposingWord(int distance) {
@@ -430,7 +439,24 @@ public final class InputLogic {
final String currentKeyboardScript, final LatinIME.UIHandler handler) {
mWordBeingCorrectedByCursor = null;
mJustRevertedACommit = false;
- final Event processedEvent = mWordComposer.processEvent(event);
+ final Event processedEvent;
+ if (currentKeyboardScript.equals(ScriptUtils.SCRIPT_HANGUL)
+ // only use the Hangul chain if codepoint may actually be Hangul
+ // todo: this whole hangul-related logic should probably be somewhere else
+ // need to use hangul combiner for whitespace, because otherwise the current word
+ // seems to get deleted / replaced by space during mConnection.endBatchEdit()
+ // similar for functional keys (codePoint -1)
+ && (event.getMCodePoint() >= 0x1100 || Character.isWhitespace(event.getMCodePoint()) || event.getMCodePoint() == -1)) {
+ mWordComposer.setHangul(true);
+ final Event hangulDecodedEvent = HangulEventDecoder.decodeSoftwareKeyEvent(event);
+ // todo: here hangul combiner does already consume the event, and appends typed codepoint
+ // to the current word instead of considering the cursor position
+ // position is actually not visible to the combiner, how to fix?
+ processedEvent = mWordComposer.processEvent(hangulDecodedEvent);
+ } else {
+ mWordComposer.setHangul(false);
+ processedEvent = mWordComposer.processEvent(event);
+ }
final InputTransaction inputTransaction = new InputTransaction(settingsValues,
processedEvent, SystemClock.uptimeMillis(), mSpaceState,
getActualCapsMode(settingsValues, keyboardShiftMode));
@@ -649,8 +675,7 @@ public final class InputLogic {
*/
private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction,
final String currentKeyboardScript, final LatinIME.UIHandler handler) {
- final int keyCode = event.getMKeyCode();
- switch (keyCode) {
+ switch (event.getMKeyCode()) {
case KeyCode.DELETE:
handleBackspaceEvent(event, inputTransaction, currentKeyboardScript);
// Backspace is a functional key, but it affects the contents of the editor.
@@ -693,7 +718,7 @@ public final class InputLogic {
case KeyCode.SHIFT_ENTER:
// todo: try using sendDownUpKeyEventWithMetaState() and remove the key code maybe
final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
- keyCode, 0, event.getMX(), event.getMY(), event.isKeyRepeat());
+ event.getMKeyCode(), 0, event.getMX(), event.getMY(), event.isKeyRepeat());
handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler);
// Shift + Enter is treated as a functional key but it results in adding a new
// line, so that does affect the contents of the editor.
@@ -724,48 +749,23 @@ public final class InputLogic {
if (mConnection.hasSelection()) {
mConnection.copyText(true);
// fake delete keypress to remove the text
- final Event backspaceEvent = Event.createSoftwareKeypressEvent(KeyCode.DELETE, 0,
+ final Event backspaceEvent = LatinIME.createSoftwareKeypressEvent(KeyCode.DELETE, 0,
event.getMX(), event.getMY(), event.isKeyRepeat());
handleBackspaceEvent(backspaceEvent, inputTransaction, currentKeyboardScript);
inputTransaction.setDidAffectContents();
}
break;
case KeyCode.WORD_LEFT:
- sendDownUpKeyEventWithMetaState(
- ScriptUtils.isScriptRtl(currentKeyboardScript) ? KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT,
- KeyEvent.META_CTRL_ON | event.getMMetaState());
+ sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_CTRL_ON);
break;
case KeyCode.WORD_RIGHT:
- sendDownUpKeyEventWithMetaState(
- ScriptUtils.isScriptRtl(currentKeyboardScript) ? KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT,
- KeyEvent.META_CTRL_ON | event.getMMetaState());
+ sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.META_CTRL_ON);
break;
case KeyCode.MOVE_START_OF_PAGE:
- final int selectionEnd1 = mConnection.getExpectedSelectionEnd();
- final int selectionStart1 = mConnection.getExpectedSelectionStart();
- sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.META_CTRL_ON | event.getMMetaState());
- if (mConnection.getExpectedSelectionStart() == selectionStart1 && mConnection.getExpectedSelectionEnd() == selectionEnd1) {
- // unchanged -> try a different method (necessary for compose fields)
- final int newEnd = (event.getMMetaState() & KeyEvent.META_SHIFT_MASK) != 0 ? selectionEnd1 : 0;
- mConnection.setSelection(0, newEnd);
- }
+ sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.META_CTRL_ON);
break;
case KeyCode.MOVE_END_OF_PAGE:
- final int selectionStart2 = mConnection.getExpectedSelectionStart();
- final int selectionEnd2 = mConnection.getExpectedSelectionEnd();
- sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_END, KeyEvent.META_CTRL_ON | event.getMMetaState());
- if (mConnection.getExpectedSelectionStart() == selectionStart2 && mConnection.getExpectedSelectionEnd() == selectionEnd2) {
- // unchanged, try fallback e.g. for compose fields that don't care about ctrl + end
- // we just move to a very large index, and hope the field is prepared to deal with this
- // getting the actual length of the text for setting the correct position can be tricky for some apps...
- try {
- final int newStart = (event.getMMetaState() & KeyEvent.META_SHIFT_MASK) != 0 ? selectionStart2 : Integer.MAX_VALUE;
- mConnection.setSelection(newStart, Integer.MAX_VALUE);
- } catch (Exception e) {
- // better catch potential errors and just do nothing in this case
- Log.i(TAG, "error when trying to move cursor to last position: " + e);
- }
- }
+ sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_END, KeyEvent.META_CTRL_ON);
break;
case KeyCode.UNDO:
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_Z, KeyEvent.META_CTRL_ON);
@@ -779,33 +779,31 @@ public final class InputLogic {
case KeyCode.TIMESTAMP:
mLatinIME.onTextInput(TimestampKt.getTimestamp(mLatinIME));
break;
- case KeyCode.SEND_INTENT_ONE, KeyCode.SEND_INTENT_TWO, KeyCode.SEND_INTENT_THREE:
- IntentUtils.handleSendIntentKey(mLatinIME, event.getMKeyCode());
- case KeyCode.IME_HIDE_UI:
- mLatinIME.requestHideSelf(0);
- break;
case KeyCode.VOICE_INPUT:
// switching to shortcut IME, shift state, keyboard,... is handled by LatinIME,
// {@link KeyboardSwitcher#onEvent(Event)}, or {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
// We need to switch to the shortcut IME. This is handled by LatinIME since the
// input logic has no business with IME switching.
- case KeyCode.CAPS_LOCK, KeyCode.EMOJI, KeyCode.TOGGLE_ONE_HANDED_MODE, KeyCode.SWITCH_ONE_HANDED_MODE:
+ case KeyCode.CAPS_LOCK, KeyCode.SYMBOL_ALPHA, KeyCode.ALPHA, KeyCode.SYMBOL, KeyCode.NUMPAD, KeyCode.EMOJI,
+ KeyCode.TOGGLE_ONE_HANDED_MODE, KeyCode.SWITCH_ONE_HANDED_MODE,
+ KeyCode.CTRL, KeyCode.ALT, KeyCode.FN, KeyCode.META:
break;
default:
- if (KeyCode.INSTANCE.isModifier(keyCode))
- return; // continuation of previous switch case above, but modifiers are held in a separate place
- final int keyEventCode = keyCode > 0
- ? keyCode
- : event.getMCodePoint() >= 0 ? KeyCode.codePointToKeyEventCode(event.getMCodePoint())
- : KeyCode.keyCodeToKeyEventCode(keyCode);
- if (keyEventCode != KeyEvent.KEYCODE_UNKNOWN) {
- sendDownUpKeyEventWithMetaState(keyEventCode, event.getMMetaState());
- return;
+ if (event.getMMetaState() != 0) {
+ // need to convert codepoint to KeyEvent.KEYCODE_
+ final int codeToConvert = event.getMKeyCode() < 0 ? event.getMKeyCode() : event.getMCodePoint();
+ int keyEventCode = KeyCode.INSTANCE.toKeyEventCode(codeToConvert);
+ if (keyEventCode != KeyEvent.KEYCODE_UNKNOWN)
+ sendDownUpKeyEventWithMetaState(keyEventCode, event.getMMetaState());
+ return; // never crash if user inputs sth we don't have a KeyEvent.KEYCODE for
+ } else if (event.getMKeyCode() < 0) {
+ int keyEventCode = KeyCode.INSTANCE.toKeyEventCode(event.getMKeyCode());
+ if (keyEventCode != KeyEvent.KEYCODE_UNKNOWN) {
+ sendDownUpKeyEvent(keyEventCode);
+ return;
+ }
}
- // unknown event
- Log.e(TAG, "unknown event, key code: "+keyCode+", meta: "+event.getMMetaState());
- if (DebugFlags.DEBUG_ENABLED)
- throw new RuntimeException("Unknown event");
+ throw new RuntimeException("Unknown key code : " + event.getMKeyCode());
}
}
@@ -1652,24 +1650,26 @@ public final class InputLogic {
}
final AsyncResultHolder holder = new AsyncResultHolder<>("Suggest");
- mInputLogicHandler.getSuggestedWords(() -> getSuggestedWords(
- inputStyle, SuggestedWords.NOT_A_SEQUENCE_NUMBER,
- suggestedWords -> {
- final String typedWordString = mWordComposer.getTypedWord();
- final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo(
- typedWordString, "", SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED,
- Dictionary.DICTIONARY_USER_TYPED, SuggestedWordInfo.NOT_AN_INDEX, SuggestedWordInfo.NOT_A_CONFIDENCE
- );
- // Show new suggestions if we have at least one. Otherwise keep the old
- // suggestions with the new typed word. Exception: if the length of the
- // typed word is <= 1 (after a deletion typically) we clear old suggestions.
- if (suggestedWords.size() > 1 || typedWordString.length() <= 1) {
- holder.set(suggestedWords);
- } else {
- holder.set(retrieveOlderSuggestions(typedWordInfo, mSuggestedWords));
+ mInputLogicHandler.getSuggestedWords(inputStyle, SuggestedWords.NOT_A_SEQUENCE_NUMBER,
+ suggestedWords -> {
+ final String typedWordString = mWordComposer.getTypedWord();
+ final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo(
+ typedWordString, "" /* prevWordsContext */,
+ SuggestedWordInfo.MAX_SCORE,
+ SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+ SuggestedWordInfo.NOT_A_CONFIDENCE);
+ // Show new suggestions if we have at least one. Otherwise keep the old
+ // suggestions with the new typed word. Exception: if the length of the
+ // typed word is <= 1 (after a deletion typically) we clear old suggestions.
+ if (suggestedWords.size() > 1 || typedWordString.length() <= 1) {
+ holder.set(suggestedWords);
+ } else {
+ holder.set(retrieveOlderSuggestions(typedWordInfo, mSuggestedWords));
+ }
}
- }
- ));
+ );
+
// This line may cause the current thread to wait.
final SuggestedWords suggestedWords = holder.get(null,
Constants.GET_SUGGESTED_WORDS_TIMEOUT);
@@ -1779,8 +1779,8 @@ public final class InputLogic {
// If there weren't any suggestion spans on this word, suggestions#size() will be 1
// if shouldIncludeResumedWordInSuggestions is true, 0 otherwise. In this case, we
// have no useful suggestions, so we will try to compute some for it instead.
- mInputLogicHandler.getSuggestedWords(() -> getSuggestedWords(Suggest.SESSION_ID_TYPING,
- SuggestedWords.NOT_A_SEQUENCE_NUMBER, this::doShowSuggestionsAndClearAutoCorrectionIndicator));
+ mInputLogicHandler.getSuggestedWords(Suggest.SESSION_ID_TYPING,
+ SuggestedWords.NOT_A_SEQUENCE_NUMBER, this::doShowSuggestionsAndClearAutoCorrectionIndicator);
} else {
// We found suggestion spans in the word. We'll create the SuggestedWords out of
// them, and make willAutoCorrect false. We make typedWordValid false, because the
@@ -2404,17 +2404,12 @@ public final class InputLogic {
return true;
}
- // we used to provide keyboard, settingsValues and keyboardShiftMode, but every time read it from current instance anyway
- public void getSuggestedWords(final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
- final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard();
- if (keyboard == null) {
- callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance());
- return;
- }
- final SettingsValues settingsValues = Settings.getValues();
+ public void getSuggestedWords(final SettingsValues settingsValues,
+ final Keyboard keyboard, final int keyboardShiftMode, final int inputStyle,
+ final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
mWordComposer.adviseCapitalizedModeBeforeFetchingSuggestions(
- getActualCapsMode(settingsValues, KeyboardSwitcher.getInstance().getKeyboardShiftMode()));
- final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
+ getActualCapsMode(settingsValues, keyboardShiftMode));
+ mSuggest.getSuggestedWords(mWordComposer,
getNgramContextFromNthPreviousWordForSuggestion(
settingsValues.mSpacingAndPunctuations,
// Get the word on which we should search the bigrams. If we are composing
@@ -2424,8 +2419,7 @@ public final class InputLogic {
keyboard,
settingsValues.mSettingsValuesForSuggestion,
settingsValues.mAutoCorrectEnabled,
- inputStyle, sequenceNumber);
- callback.onGetSuggestedWords(suggestedWords);
+ inputStyle, sequenceNumber, callback);
}
/**
diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.java
index 6e728a5a7..99b77fa3e 100644
--- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.java
+++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.java
@@ -11,6 +11,7 @@ import android.os.HandlerThread;
import android.os.Message;
import helium314.keyboard.latin.LatinIME;
+import helium314.keyboard.latin.Suggest.OnGetSuggestedWordsCallback;
import helium314.keyboard.latin.SuggestedWords;
import helium314.keyboard.latin.common.InputPointers;
@@ -19,19 +20,48 @@ import helium314.keyboard.latin.common.InputPointers;
*/
class InputLogicHandler implements Handler.Callback {
final Handler mNonUIThreadHandler;
- final LatinIME.UIHandler mLatinIMEHandler;
+ // TODO: remove this reference.
+ final LatinIME mLatinIME;
final InputLogic mInputLogic;
private final Object mLock = new Object();
private boolean mInBatchInput; // synchronized using {@link #mLock}.
private static final int MSG_GET_SUGGESTED_WORDS = 1;
- public InputLogicHandler(final LatinIME.UIHandler latinIMEHandler, final InputLogic inputLogic) {
+ // A handler that never does anything. This is used for cases where events come before anything
+ // is initialized, though probably only the monkey can actually do this.
+ public static final InputLogicHandler NULL_HANDLER = new InputLogicHandler() {
+ @Override
+ public void reset() {}
+ @Override
+ public boolean handleMessage(final Message msg) { return true; }
+ @Override
+ public void onStartBatchInput() {}
+ @Override
+ public void onUpdateBatchInput(final InputPointers batchPointers,
+ final int sequenceNumber) {}
+ @Override
+ public void onCancelBatchInput() {}
+ @Override
+ public void updateTailBatchInput(final InputPointers batchPointers,
+ final int sequenceNumber) {}
+ @Override
+ public void getSuggestedWords(final int sessionId, final int sequenceNumber,
+ final OnGetSuggestedWordsCallback callback) {}
+ };
+
+ InputLogicHandler() {
+ mNonUIThreadHandler = null;
+ mLatinIME = null;
+ mInputLogic = null;
+ }
+
+ public InputLogicHandler(final LatinIME latinIME, final InputLogic inputLogic) {
final HandlerThread handlerThread = new HandlerThread(
InputLogicHandler.class.getSimpleName());
handlerThread.start();
mNonUIThreadHandler = new Handler(handlerThread.getLooper(), this);
- mLatinIMEHandler = latinIMEHandler;
+ mLatinIME = latinIME;
mInputLogic = inputLogic;
}
@@ -39,6 +69,12 @@ class InputLogicHandler implements Handler.Callback {
mNonUIThreadHandler.removeCallbacksAndMessages(null);
}
+ // In unit tests, we create several instances of LatinIME, which results in several instances
+ // of InputLogicHandler. To avoid these handlers lingering, we call this.
+ public void destroy() {
+ mNonUIThreadHandler.getLooper().quitSafely();
+ }
+
/**
* Handle a message.
* @see android.os.Handler.Callback#handleMessage(android.os.Message)
@@ -47,7 +83,7 @@ class InputLogicHandler implements Handler.Callback {
@Override
public boolean handleMessage(final Message msg) {
if (msg.what == MSG_GET_SUGGESTED_WORDS)
- ((Runnable)msg.obj).run();
+ mLatinIME.getSuggestedWords(msg.arg1, msg.arg2, (OnGetSuggestedWordsCallback) msg.obj);
return true;
}
@@ -82,14 +118,13 @@ class InputLogicHandler implements Handler.Callback {
return;
}
mInputLogic.mWordComposer.setBatchInputPointers(batchPointers);
- getSuggestedWords(() -> mInputLogic.getSuggestedWords(
- isTailBatchInput ? SuggestedWords.INPUT_STYLE_TAIL_BATCH : SuggestedWords.INPUT_STYLE_UPDATE_BATCH, sequenceNumber,
- suggestedWords -> showGestureSuggestionsWithPreviewVisuals(suggestedWords, isTailBatchInput))
- );
+ final OnGetSuggestedWordsCallback callback = suggestedWords -> showGestureSuggestionsWithPreviewVisuals(suggestedWords, isTailBatchInput);
+ getSuggestedWords(isTailBatchInput ? SuggestedWords.INPUT_STYLE_TAIL_BATCH
+ : SuggestedWords.INPUT_STYLE_UPDATE_BATCH, sequenceNumber, callback);
}
}
- private void showGestureSuggestionsWithPreviewVisuals(final SuggestedWords suggestedWordsForBatchInput,
+ void showGestureSuggestionsWithPreviewVisuals(final SuggestedWords suggestedWordsForBatchInput,
final boolean isTailBatchInput) {
final SuggestedWords suggestedWordsToShowSuggestions;
// We're now inside the callback. This always runs on the Non-UI thread,
@@ -103,12 +138,13 @@ class InputLogicHandler implements Handler.Callback {
} else {
suggestedWordsToShowSuggestions = suggestedWordsForBatchInput;
}
- mLatinIMEHandler.showGesturePreviewAndSuggestionStrip(suggestedWordsToShowSuggestions, isTailBatchInput);
+ mLatinIME.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWordsToShowSuggestions,
+ isTailBatchInput /* dismissGestureFloatingPreviewText */);
if (isTailBatchInput) {
mInBatchInput = false;
// The following call schedules onEndBatchInputInternal
// to be called on the UI thread.
- mLatinIMEHandler.showTailBatchInputResult(suggestedWordsToShowSuggestions);
+ mLatinIME.mHandler.showTailBatchInputResult(suggestedWordsToShowSuggestions);
}
}
@@ -157,7 +193,9 @@ class InputLogicHandler implements Handler.Callback {
updateBatchInput(batchPointers, sequenceNumber, true);
}
- public void getSuggestedWords(final Runnable callback) {
- mNonUIThreadHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, callback).sendToTarget();
+ public void getSuggestedWords(final int inputStyle, final int sequenceNumber,
+ final OnGetSuggestedWordsCallback callback) {
+ mNonUIThreadHandler.obtainMessage(
+ MSG_GET_SUGGESTED_WORDS, inputStyle, sequenceNumber, callback).sendToTarget();
}
}
diff --git a/app/src/main/java/helium314/keyboard/latin/personalization/PersonalizationHelper.java b/app/src/main/java/helium314/keyboard/latin/personalization/PersonalizationHelper.java
index dc8692c6a..93f7f76cb 100644
--- a/app/src/main/java/helium314/keyboard/latin/personalization/PersonalizationHelper.java
+++ b/app/src/main/java/helium314/keyboard/latin/personalization/PersonalizationHelper.java
@@ -10,6 +10,7 @@ import android.content.Context;
import helium314.keyboard.latin.utils.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import helium314.keyboard.latin.common.FileUtils;
@@ -30,8 +31,12 @@ public class PersonalizationHelper {
sLangUserHistoryDictCache = new ConcurrentHashMap<>();
@NonNull
- public static UserHistoryDictionary getUserHistoryDictionary(final Context context, final Locale locale) {
+ public static UserHistoryDictionary getUserHistoryDictionary(
+ final Context context, final Locale locale, @Nullable final String accountName) {
String lookupStr = locale.toString();
+ if (accountName != null) {
+ lookupStr += "." + accountName;
+ }
synchronized (sLangUserHistoryDictCache) {
if (sLangUserHistoryDictCache.containsKey(lookupStr)) {
final SoftReference ref =
@@ -45,7 +50,8 @@ public class PersonalizationHelper {
return dict;
}
}
- final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale);
+ final UserHistoryDictionary dict = new UserHistoryDictionary(
+ context, locale, accountName);
sLangUserHistoryDictCache.put(lookupStr, new SoftReference<>(dict));
return dict;
}
diff --git a/app/src/main/java/helium314/keyboard/latin/personalization/UserHistoryDictionary.java b/app/src/main/java/helium314/keyboard/latin/personalization/UserHistoryDictionary.java
index 98645080e..b860b1c96 100644
--- a/app/src/main/java/helium314/keyboard/latin/personalization/UserHistoryDictionary.java
+++ b/app/src/main/java/helium314/keyboard/latin/personalization/UserHistoryDictionary.java
@@ -30,8 +30,9 @@ public class UserHistoryDictionary extends ExpandableBinaryDictionary {
static final String NAME = UserHistoryDictionary.class.getSimpleName();
// TODO: Make this constructor private
- UserHistoryDictionary(final Context context, final Locale locale) {
- super(context, getUserHistoryDictName(NAME, locale, null), locale, Dictionary.TYPE_USER_HISTORY, null);
+ UserHistoryDictionary(final Context context, final Locale locale,
+ @Nullable final String account) {
+ super(context, getUserHistoryDictName(NAME, locale, null /* dictFile */, account), locale, Dictionary.TYPE_USER_HISTORY, null);
if (mLocale != null && mLocale.toString().length() > 1) {
reloadDictionaryIfRequired();
}
@@ -40,13 +41,14 @@ public class UserHistoryDictionary extends ExpandableBinaryDictionary {
/**
* @returns the name of the {@link UserHistoryDictionary}.
*/
- static String getUserHistoryDictName(final String name, final Locale locale, @Nullable final File dictFile) {
+ static String getUserHistoryDictName(final String name, final Locale locale,
+ @Nullable final File dictFile, @Nullable final String account) {
return getDictName(name, locale, dictFile);
}
public static UserHistoryDictionary getDictionary(final Context context, final Locale locale,
- final File dictFile, final String dictNamePrefix) {
- return PersonalizationHelper.getUserHistoryDictionary(context, locale);
+ final File dictFile, final String dictNamePrefix, @Nullable final String account) {
+ return PersonalizationHelper.getUserHistoryDictionary(context, locale, account);
}
/**
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
index a73760205..377e19899 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
@@ -44,7 +44,6 @@ object Defaults {
LayoutType.CLIPBOARD_BOTTOM -> "clip_bottom_row"
}
- private const val DEFAULT_SIZE_SCALE = 1.0f // 100%
const val PREF_THEME_STYLE = KeyboardTheme.STYLE_MATERIAL
const val PREF_ICON_STYLE = KeyboardTheme.STYLE_MATERIAL
const val PREF_THEME_COLORS = KeyboardTheme.THEME_LIGHT
@@ -58,7 +57,6 @@ object Defaults {
const val PREF_VIBRATE_ON = false
const val PREF_VIBRATE_IN_DND_MODE = false
const val PREF_SOUND_ON = false
- const val PREF_SHOW_EMOJI_DESCRIPTIONS = true
@JvmField
var PREF_POPUP_ON = true
const val PREF_AUTO_CORRECTION = true
@@ -82,18 +80,15 @@ object Defaults {
"hu${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwerty"
const val PREF_ENABLE_SPLIT_KEYBOARD = false
const val PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE = false
- @JvmField
- val PREF_SPLIT_SPACER_SCALE = Array(2) { DEFAULT_SIZE_SCALE }
- @JvmField
- val PREF_KEYBOARD_HEIGHT_SCALE = Array(2) { DEFAULT_SIZE_SCALE }
- @JvmField
- val PREF_BOTTOM_PADDING_SCALE = arrayOf(DEFAULT_SIZE_SCALE, 0f)
- @JvmField
- val PREF_SIDE_PADDING_SCALE = Array(4) { 0f }
- const val PREF_FONT_SCALE = DEFAULT_SIZE_SCALE
- const val PREF_EMOJI_FONT_SCALE = DEFAULT_SIZE_SCALE
- const val PREF_EMOJI_KEY_FIT = true
- const val PREF_EMOJI_SKIN_TONE = ""
+ const val PREF_SPLIT_SPACER_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
+ const val PREF_SPLIT_SPACER_SCALE_LANDSCAPE = SettingsValues.DEFAULT_SIZE_SCALE
+ const val PREF_KEYBOARD_HEIGHT_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
+ const val PREF_BOTTOM_PADDING_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
+ const val PREF_BOTTOM_PADDING_SCALE_LANDSCAPE = 0f
+ const val PREF_SIDE_PADDING_SCALE = 0f
+ const val PREF_SIDE_PADDING_SCALE_LANDSCAPE = 0f
+ const val PREF_FONT_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
+ const val PREF_EMOJI_FONT_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
const val PREF_SPACE_HORIZONTAL_SWIPE = "move_cursor"
const val PREF_SPACE_VERTICAL_SWIPE = "none"
const val PREF_DELETE_SWIPE = true
@@ -120,7 +115,6 @@ object Defaults {
const val PREF_GESTURE_TRAIL_FADEOUT_DURATION = 800
const val PREF_SHOW_SETUP_WIZARD_ICON = true
const val PREF_USE_CONTACTS = false
- const val PREF_USE_APPS = false
const val PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD = false
const val PREF_ONE_HANDED_MODE = false
@SuppressLint("RtlHardcoded")
@@ -148,8 +142,6 @@ object Defaults {
const val PREF_SELECTED_SUBTYPE = ""
const val PREF_URL_DETECTION = false
const val PREF_DONT_SHOW_MISSING_DICTIONARY_DIALOG = false
- const val PREF_TOOLBAR_MODE = "EXPANDABLE"
- const val PREF_TOOLBAR_HIDING_GLOBAL = true
const val PREF_QUICK_PIN_TOOLBAR_KEYS = false
val PREF_PINNED_TOOLBAR_KEYS = defaultPinnedToolbarPref
val PREF_TOOLBAR_KEYS = defaultToolbarPref
@@ -163,6 +155,7 @@ object Defaults {
const val PREF_REMOVE_REDUNDANT_POPUPS = false
const val PREF_SPACE_BAR_TEXT = ""
const val PREF_TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss"
+ @JvmField
val PREF_EMOJI_MAX_SDK = Build.VERSION.SDK_INT
const val PREF_EMOJI_RECENT_KEYS = ""
const val PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID = 0
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
index cc72c893c..098b51b0d 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
@@ -37,7 +37,6 @@ import helium314.keyboard.latin.utils.StatsUtils;
import helium314.keyboard.latin.utils.SubtypeSettings;
import helium314.keyboard.latin.utils.ToolbarKey;
import helium314.keyboard.latin.utils.ToolbarUtilsKt;
-import helium314.keyboard.latin.utils.ToolbarMode;
import java.io.File;
import java.util.Arrays;
@@ -67,7 +66,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_VIBRATE_ON = "vibrate_on";
public static final String PREF_VIBRATE_IN_DND_MODE = "vibrate_in_dnd_mode";
public static final String PREF_SOUND_ON = "sound_on";
- public static final String PREF_SHOW_EMOJI_DESCRIPTIONS = "show_emoji_descriptions";
public static final String PREF_POPUP_ON = "popup_on";
public static final String PREF_AUTO_CORRECTION = "auto_correction";
public static final String PREF_MORE_AUTO_CORRECTION = "more_auto_correction";
@@ -88,21 +86,22 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_ADDITIONAL_SUBTYPES = "additional_subtypes";
public static final String PREF_ENABLE_SPLIT_KEYBOARD = "split_keyboard";
public static final String PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE = "split_keyboard_landscape";
- public static final String PREF_SPLIT_SPACER_SCALE_PREFIX = "split_spacer_scale";
- public static final String PREF_KEYBOARD_HEIGHT_SCALE_PREFIX = "keyboard_height_scale";
- public static final String PREF_BOTTOM_PADDING_SCALE_PREFIX = "bottom_padding_scale";
- public static final String PREF_SIDE_PADDING_SCALE_PREFIX = "side_padding_scale";
+ public static final String PREF_SPLIT_SPACER_SCALE = "split_spacer_scale";
+ public static final String PREF_SPLIT_SPACER_SCALE_LANDSCAPE = "split_spacer_scale_landscape";
+ public static final String PREF_KEYBOARD_HEIGHT_SCALE = "keyboard_height_scale";
+ public static final String PREF_BOTTOM_PADDING_SCALE = "bottom_padding_scale";
+ public static final String PREF_BOTTOM_PADDING_SCALE_LANDSCAPE = "bottom_padding_scale_landscape";
+ public static final String PREF_SIDE_PADDING_SCALE = "side_padding_scale";
+ public static final String PREF_SIDE_PADDING_SCALE_LANDSCAPE = "side_padding_scale_landscape";
public static final String PREF_FONT_SCALE = "font_scale";
public static final String PREF_EMOJI_FONT_SCALE = "emoji_font_scale";
- public static final String PREF_EMOJI_KEY_FIT = "emoji_key_fit";
- public static final String PREF_EMOJI_SKIN_TONE = "emoji_skin_tone";
public static final String PREF_SPACE_HORIZONTAL_SWIPE = "horizontal_space_swipe";
public static final String PREF_SPACE_VERTICAL_SWIPE = "vertical_space_swipe";
public static final String PREF_DELETE_SWIPE = "delete_swipe";
public static final String PREF_AUTOSPACE_AFTER_PUNCTUATION = "autospace_after_punctuation";
public static final String PREF_AUTOSPACE_AFTER_SUGGESTION = "autospace_after_suggestion";
public static final String PREF_AUTOSPACE_AFTER_GESTURE_TYPING = "autospace_after_gesture_typing";
- public static final String PREF_AUTOSPACE_BEFORE_GESTURE_TYPING = "autospace_before_gesture_typing";
+ public static final String PREF_AUTOSPACE_BEFORE_GESTURE_TYPING = "autospace_before_gesture_typing";
public static final String PREF_SHIFT_REMOVES_AUTOSPACE = "shift_removes_autospace";
public static final String PREF_ALWAYS_INCOGNITO_MODE = "always_incognito_mode";
public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
@@ -121,12 +120,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_GESTURE_TRAIL_FADEOUT_DURATION = "gesture_trail_fadeout_duration";
public static final String PREF_SHOW_SETUP_WIZARD_ICON = "show_setup_wizard_icon";
public static final String PREF_USE_CONTACTS = "use_contacts";
- public static final String PREF_USE_APPS = "use_apps";
public static final String PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD = "long_press_symbols_for_numpad";
- public static final String PREF_ONE_HANDED_MODE_PREFIX = "one_handed_mode_enabled";
- public static final String PREF_ONE_HANDED_GRAVITY_PREFIX = "one_handed_mode_gravity";
- public static final String PREF_ONE_HANDED_SCALE_PREFIX = "one_handed_mode_scale";
+ // one-handed mode gravity, enablement and scale, stored separately per orientation
+ public static final String PREF_ONE_HANDED_MODE_PREFIX = "one_handed_mode_enabled_p_";
+ public static final String PREF_ONE_HANDED_GRAVITY_PREFIX = "one_handed_mode_gravity_p_";
+ public static final String PREF_ONE_HANDED_SCALE_PREFIX = "one_handed_mode_scale_p_";
public static final String PREF_SHOW_NUMBER_ROW = "show_number_row";
public static final String PREF_LOCALIZED_NUMBER_ROW = "localized_number_row";
@@ -166,8 +165,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_REMOVE_REDUNDANT_POPUPS = "remove_redundant_popups";
public static final String PREF_SPACE_BAR_TEXT = "space_bar_text";
public static final String PREF_TIMESTAMP_FORMAT = "timestamp_format";
- public static final String PREF_TOOLBAR_MODE = "toolbar_mode";
- public static final String PREF_TOOLBAR_HIDING_GLOBAL = "toolbar_hiding_global";
// Emoji
public static final String PREF_EMOJI_MAX_SDK = "emoji_max_sdk";
@@ -324,10 +321,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
mPrefs.edit().putBoolean(Settings.PREF_ALWAYS_INCOGNITO_MODE, !oldValue).apply();
}
- public static ToolbarMode readToolbarMode(final SharedPreferences prefs) {
- return ToolbarMode.valueOf(prefs.getString(PREF_TOOLBAR_MODE, Defaults.PREF_TOOLBAR_MODE));
- }
-
public static int readHorizontalSpaceSwipe(final SharedPreferences prefs) {
return switch (prefs.getString(PREF_SPACE_HORIZONTAL_SWIPE, Defaults.PREF_SPACE_HORIZONTAL_SWIPE)) {
case "move_cursor" -> KeyboardActionListener.SWIPE_MOVE_CURSOR;
@@ -362,43 +355,31 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return prefs.getBoolean(PREF_SHOW_SETUP_WIZARD_ICON, Defaults.PREF_SHOW_SETUP_WIZARD_ICON);
}
- public static boolean readOneHandedModeEnabled(final SharedPreferences prefs, final boolean landscape, final boolean split) {
- final int index = SettingsKt.findIndexOfDefaultSetting(landscape, split);
- final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_MODE_PREFIX, index, 2);
- return prefs.getBoolean(key, Defaults.PREF_ONE_HANDED_MODE);
+ public static boolean readOneHandedModeEnabled(final SharedPreferences prefs, final boolean isLandscape) {
+ return prefs.getBoolean(PREF_ONE_HANDED_MODE_PREFIX + !isLandscape, Defaults.PREF_ONE_HANDED_MODE);
}
public void writeOneHandedModeEnabled(final boolean enabled) {
- final boolean landscape = mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE;
- final int index = SettingsKt.findIndexOfDefaultSetting(landscape, mSettingsValues.mIsSplitKeyboardEnabled);
- final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_MODE_PREFIX, index, 2);
- mPrefs.edit().putBoolean(key, enabled).apply();
+ mPrefs.edit().putBoolean(PREF_ONE_HANDED_MODE_PREFIX +
+ (mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), enabled).apply();
}
- public static float readOneHandedModeScale(final SharedPreferences prefs, final boolean landscape, final boolean split) {
- final int index = SettingsKt.findIndexOfDefaultSetting(landscape, split);
- final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_SCALE_PREFIX, index, 2);
- return prefs.getFloat(key, Defaults.PREF_ONE_HANDED_SCALE);
+ public static float readOneHandedModeScale(final SharedPreferences prefs, final boolean isLandscape) {
+ return prefs.getFloat(PREF_ONE_HANDED_SCALE_PREFIX + !isLandscape, Defaults.PREF_ONE_HANDED_SCALE);
}
public void writeOneHandedModeScale(final Float scale) {
- final boolean landscape = mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE;
- final int index = SettingsKt.findIndexOfDefaultSetting(landscape, mSettingsValues.mIsSplitKeyboardEnabled);
- final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_SCALE_PREFIX, index, 2);
- mPrefs.edit().putFloat(key, scale).apply();
+ mPrefs.edit().putFloat(PREF_ONE_HANDED_SCALE_PREFIX +
+ (mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), scale).apply();
}
- public static int readOneHandedModeGravity(final SharedPreferences prefs, final boolean landscape, final boolean split) {
- final int index = SettingsKt.findIndexOfDefaultSetting(landscape, split);
- final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_GRAVITY_PREFIX, index, 2);
- return prefs.getInt(key, Defaults.PREF_ONE_HANDED_GRAVITY);
+ public static int readOneHandedModeGravity(final SharedPreferences prefs, final boolean isLandscape) {
+ return prefs.getInt(PREF_ONE_HANDED_GRAVITY_PREFIX + !isLandscape, Defaults.PREF_ONE_HANDED_GRAVITY);
}
public void writeOneHandedModeGravity(final int gravity) {
- final boolean landscape = mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE;
- final int index = SettingsKt.findIndexOfDefaultSetting(landscape, mSettingsValues.mIsSplitKeyboardEnabled);
- final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_GRAVITY_PREFIX, index, 2);
- mPrefs.edit().putInt(key, gravity).apply();
+ mPrefs.edit().putInt(PREF_ONE_HANDED_GRAVITY_PREFIX +
+ (mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), gravity).apply();
}
public void writeSplitKeyboardEnabled(final boolean enabled, final boolean isLandscape) {
@@ -411,32 +392,21 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return prefs.getBoolean(pref, isLandscape ? Defaults.PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE : Defaults.PREF_ENABLE_SPLIT_KEYBOARD);
}
- public static float readSplitSpacerScale(final SharedPreferences prefs, final boolean landscape) {
- final int index = SettingsKt.findIndexOfDefaultSetting(landscape);
- final Float[] defaults = Defaults.PREF_SPLIT_SPACER_SCALE;
- final float defaultValue = defaults[index];
- return prefs.getFloat(SettingsKt.createPrefKeyForBooleanSettings(PREF_SPLIT_SPACER_SCALE_PREFIX, index, 1), defaultValue);
+ public static float readSplitSpacerScale(final SharedPreferences prefs, final boolean isLandscape) {
+ final String pref = isLandscape ? PREF_SPLIT_SPACER_SCALE_LANDSCAPE : PREF_SPLIT_SPACER_SCALE;
+ return prefs.getFloat(pref, isLandscape ? Defaults.PREF_SPLIT_SPACER_SCALE_LANDSCAPE : Defaults.PREF_SPLIT_SPACER_SCALE);
}
public static float readBottomPaddingScale(final SharedPreferences prefs, final boolean landscape) {
- final int index = SettingsKt.findIndexOfDefaultSetting(landscape);
- final Float[] defaults = Defaults.PREF_BOTTOM_PADDING_SCALE;
- final float defaultValue = defaults[index];
- return prefs.getFloat(SettingsKt.createPrefKeyForBooleanSettings(PREF_BOTTOM_PADDING_SCALE_PREFIX, index, 1), defaultValue);
+ if (landscape)
+ return prefs.getFloat(PREF_BOTTOM_PADDING_SCALE_LANDSCAPE, Defaults.PREF_BOTTOM_PADDING_SCALE_LANDSCAPE);
+ return prefs.getFloat(PREF_BOTTOM_PADDING_SCALE, Defaults.PREF_BOTTOM_PADDING_SCALE);
}
- public static float readSidePaddingScale(final SharedPreferences prefs, final boolean landscape, final boolean split) {
- final int index = SettingsKt.findIndexOfDefaultSetting(landscape, split);
- final Float[] defaults = Defaults.PREF_SIDE_PADDING_SCALE;
- final float defaultValue = defaults[index];
- return prefs.getFloat(SettingsKt.createPrefKeyForBooleanSettings(PREF_SIDE_PADDING_SCALE_PREFIX, index, 2), defaultValue);
- }
-
- public static float readHeightScale(final SharedPreferences prefs, final boolean landscape) {
- final int index = SettingsKt.findIndexOfDefaultSetting(landscape);
- final Float[] defaults = Defaults.PREF_KEYBOARD_HEIGHT_SCALE;
- final float defaultValue = defaults[index];
- return prefs.getFloat(SettingsKt.createPrefKeyForBooleanSettings(PREF_KEYBOARD_HEIGHT_SCALE_PREFIX, index, 1), defaultValue);
+ public static float readSidePaddingScale(final SharedPreferences prefs, final boolean landscape) {
+ if (landscape)
+ return prefs.getFloat(PREF_SIDE_PADDING_SCALE_LANDSCAPE, Defaults.PREF_SIDE_PADDING_SCALE_LANDSCAPE);
+ return prefs.getFloat(PREF_SIDE_PADDING_SCALE, Defaults.PREF_SIDE_PADDING_SCALE);
}
public static boolean readHasHardwareKeyboard(final Configuration conf) {
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt b/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt
index d532a16a3..5a472d058 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt
@@ -14,13 +14,3 @@ fun customIconIds(context: Context, prefs: SharedPreferences) = customIconNames(
val id = runCatching { context.resources.getIdentifier(entry.value, "drawable", context.packageName) }.getOrNull()
id?.let { entry.key to it }
}
-
-/** Derive an index from a number of boolean [settingValues], used to access the matching default value in a defaults arraY */
-fun findIndexOfDefaultSetting(vararg settingValues: Boolean): Int {
- var i = -1
- return settingValues.sumOf { i++; if (it) 1.shl(i) else 0 }
-}
-
-/** Create pref key that is derived from a [number] of boolean conditions. The [index] is as created by [findIndexOfDefaultSetting]. */
-fun createPrefKeyForBooleanSettings(prefix: String, index: Int, number: Int): String =
- "${prefix}_${Array(number) { index.shr(it) % 2 == 1 }.joinToString("_")}"
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
index f750f1072..9fe0a3725 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
+++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
@@ -16,6 +16,7 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.core.util.TypedValueCompat;
import helium314.keyboard.compat.ConfigurationCompatKt;
@@ -31,7 +32,6 @@ import helium314.keyboard.latin.utils.JniUtils;
import helium314.keyboard.latin.utils.ScriptUtils;
import helium314.keyboard.latin.utils.SubtypeSettings;
import helium314.keyboard.latin.utils.SubtypeUtilsKt;
-import helium314.keyboard.latin.utils.ToolbarMode;
import java.util.List;
import java.util.Locale;
@@ -42,6 +42,8 @@ import java.util.Locale;
*/
// Non-final for testing via mock library.
public class SettingsValues {
+ public static final float DEFAULT_SIZE_SCALE = 1.0f; // 100%
+
// From resources:
public final SpacingAndPunctuations mSpacingAndPunctuations;
public final long mDoubleSpacePeriodTimeout;
@@ -54,7 +56,6 @@ public class SettingsValues {
public final boolean mVibrateOn;
public final boolean mVibrateInDndMode;
public final boolean mSoundOn;
- public final boolean mShowEmojiDescriptions;
public final boolean mKeyPreviewPopupOn;
public final boolean mShowsVoiceInputKey;
public final boolean mLanguageSwitchKeyToOtherImes;
@@ -108,14 +109,11 @@ public class SettingsValues {
public final int mScreenMetrics;
public final boolean mAddToPersonalDictionary;
public final boolean mUseContactsDictionary;
- public final boolean mUseAppsDictionary;
public final boolean mCustomNavBarColor;
public final float mKeyboardHeightScale;
public final boolean mUrlDetectionEnabled;
public final float mBottomPaddingScale;
public final float mSidePaddingScale;
- public final ToolbarMode mToolbarMode;
- public final boolean mToolbarHidingGlobal;
public final boolean mAutoShowToolbar;
public final boolean mAutoHideToolbar;
public final boolean mAlphaAfterEmojiInEmojiView;
@@ -126,15 +124,12 @@ public class SettingsValues {
public final String mSpaceBarText;
public final float mFontSizeMultiplier;
public final float mFontSizeMultiplierEmoji;
- public final boolean mEmojiKeyFit;
// From the input box
@NonNull
public final InputAttributes mInputAttributes;
// Deduced settings
- public final boolean mSuggestionStripHiddenPerUserSettings;
- public final boolean mSecondaryStripVisible;
public final int mKeypressVibrationDuration;
public final float mKeypressSoundVolume;
public final boolean mAutoCorrectionEnabledPerUserSettings;
@@ -149,10 +144,14 @@ public class SettingsValues {
public final SettingsValuesForSuggestion mSettingsValuesForSuggestion;
public final boolean mIncognitoModeEnabled;
public final boolean mLongPressSymbolsForNumpad;
+ public final int mEmojiMaxSdk;
// User-defined colors
public final Colors mColors;
+ @Nullable
+ public final String mAccount; // todo: always null, remove?
+
// creation of Colors and SpacingAndPunctuations are the slowest parts in here, but still ok
public SettingsValues(final Context context, final SharedPreferences prefs, final Resources res,
@NonNull final InputAttributes inputAttributes) {
@@ -164,13 +163,10 @@ public class SettingsValues {
mInputAttributes = inputAttributes;
// Get the settings preferences
- mToolbarMode = Settings.readToolbarMode(prefs);
- mToolbarHidingGlobal = prefs.getBoolean(Settings.PREF_TOOLBAR_HIDING_GLOBAL, Defaults.PREF_TOOLBAR_HIDING_GLOBAL);
mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, Defaults.PREF_AUTO_CAP) && ScriptUtils.scriptSupportsUppercase(mLocale);
mVibrateOn = Settings.readVibrationEnabled(prefs);
mVibrateInDndMode = prefs.getBoolean(Settings.PREF_VIBRATE_IN_DND_MODE, Defaults.PREF_VIBRATE_IN_DND_MODE);
mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, Defaults.PREF_SOUND_ON);
- mShowEmojiDescriptions = prefs.getBoolean(Settings.PREF_SHOW_EMOJI_DESCRIPTIONS, Defaults.PREF_SHOW_EMOJI_DESCRIPTIONS);
mKeyPreviewPopupOn = prefs.getBoolean(Settings.PREF_POPUP_ON, Defaults.PREF_POPUP_ON);
mSlidingKeyInputPreviewEnabled = prefs.getBoolean(
DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW, Defaults.PREF_SLIDING_KEY_INPUT_PREVIEW);
@@ -187,7 +183,7 @@ public class SettingsValues {
mShowTldPopupKeys = prefs.getBoolean(Settings.PREF_SHOW_TLD_POPUP_KEYS, Defaults.PREF_SHOW_TLD_POPUP_KEYS);
mSpaceForLangChange = prefs.getBoolean(Settings.PREF_SPACE_TO_CHANGE_LANG, Defaults.PREF_SPACE_TO_CHANGE_LANG);
mShowsEmojiKey = prefs.getBoolean(Settings.PREF_SHOW_EMOJI_KEY, Defaults.PREF_SHOW_EMOJI_KEY);
- mVarToolbarDirection = mToolbarMode != ToolbarMode.HIDDEN && prefs.getBoolean(Settings.PREF_VARIABLE_TOOLBAR_DIRECTION, Defaults.PREF_VARIABLE_TOOLBAR_DIRECTION);
+ mVarToolbarDirection = prefs.getBoolean(Settings.PREF_VARIABLE_TOOLBAR_DIRECTION, Defaults.PREF_VARIABLE_TOOLBAR_DIRECTION);
mUsePersonalizedDicts = prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, Defaults.PREF_KEY_USE_PERSONALIZED_DICTS);
mUseDoubleSpacePeriod = prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, Defaults.PREF_KEY_USE_DOUBLE_SPACE_PERIOD)
&& inputAttributes.mIsGeneralTextInput;
@@ -216,7 +212,7 @@ public class SettingsValues {
mSplitKeyboardSpacerRelativeWidth = mIsSplitKeyboardEnabled
? Math.min(Math.max((displayWidthDp - 600) / 600f + 0.15f, 0.15f), 0.35f) * Settings.readSplitSpacerScale(prefs, isLandscape)
: 0f;
- mQuickPinToolbarKeys = mToolbarMode == ToolbarMode.EXPANDABLE && prefs.getBoolean(Settings.PREF_QUICK_PIN_TOOLBAR_KEYS, Defaults.PREF_QUICK_PIN_TOOLBAR_KEYS);
+ mQuickPinToolbarKeys = prefs.getBoolean(Settings.PREF_QUICK_PIN_TOOLBAR_KEYS, Defaults.PREF_QUICK_PIN_TOOLBAR_KEYS);
mScreenMetrics = Settings.readScreenMetrics(res);
// Compute other readable settings
@@ -231,18 +227,17 @@ public class SettingsValues {
mGestureFloatingPreviewDynamicEnabled = Settings.readGestureDynamicPreviewEnabled(prefs);
mGestureFastTypingCooldown = prefs.getInt(Settings.PREF_GESTURE_FAST_TYPING_COOLDOWN, Defaults.PREF_GESTURE_FAST_TYPING_COOLDOWN);
mGestureTrailFadeoutDuration = prefs.getInt(Settings.PREF_GESTURE_TRAIL_FADEOUT_DURATION, Defaults.PREF_GESTURE_TRAIL_FADEOUT_DURATION);
- mSuggestionStripHiddenPerUserSettings = mToolbarMode == ToolbarMode.HIDDEN || mToolbarMode == ToolbarMode.TOOLBAR_KEYS;
+ mAccount = null; // remove? or can it be useful somewhere?
mOverrideShowingSuggestions = mInputAttributes.mMayOverrideShowingSuggestions
&& prefs.getBoolean(Settings.PREF_ALWAYS_SHOW_SUGGESTIONS, Defaults.PREF_ALWAYS_SHOW_SUGGESTIONS)
&& ((inputAttributes.mInputType & InputType.TYPE_MASK_VARIATION) != InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
|| !prefs.getBoolean(Settings.PREF_ALWAYS_SHOW_SUGGESTIONS_EXCEPT_WEB_TEXT, Defaults.PREF_ALWAYS_SHOW_SUGGESTIONS_EXCEPT_WEB_TEXT));
final boolean suggestionsEnabled = prefs.getBoolean(Settings.PREF_SHOW_SUGGESTIONS, Defaults.PREF_SHOW_SUGGESTIONS);
- mSuggestionsEnabledPerUserSettings = ((mInputAttributes.mShouldShowSuggestions && suggestionsEnabled)
- || mOverrideShowingSuggestions) && !mSuggestionStripHiddenPerUserSettings;
- mSecondaryStripVisible = mToolbarMode != ToolbarMode.HIDDEN || ! mToolbarHidingGlobal;
+ mSuggestionsEnabledPerUserSettings = (mInputAttributes.mShouldShowSuggestions && suggestionsEnabled)
+ || mOverrideShowingSuggestions;
mIncognitoModeEnabled = prefs.getBoolean(Settings.PREF_ALWAYS_INCOGNITO_MODE, Defaults.PREF_ALWAYS_INCOGNITO_MODE) || mInputAttributes.mNoLearning
|| mInputAttributes.mIsPasswordField;
- mKeyboardHeightScale = Settings.readHeightScale(prefs, isLandscape);
+ mKeyboardHeightScale = prefs.getFloat(Settings.PREF_KEYBOARD_HEIGHT_SCALE, Defaults.PREF_KEYBOARD_HEIGHT_SCALE);
mSpaceSwipeHorizontal = Settings.readHorizontalSpaceSwipe(prefs);
mSpaceSwipeVertical = Settings.readVerticalSpaceSwipe(prefs);
mLanguageSwipeDistance = prefs.getInt(Settings.PREF_LANGUAGE_SWIPE_DISTANCE, Defaults.PREF_LANGUAGE_SWIPE_DISTANCE);
@@ -255,11 +250,11 @@ public class SettingsValues {
mClipboardHistoryEnabled = prefs.getBoolean(Settings.PREF_ENABLE_CLIPBOARD_HISTORY, Defaults.PREF_ENABLE_CLIPBOARD_HISTORY);
mClipboardHistoryRetentionTime = prefs.getInt(Settings.PREF_CLIPBOARD_HISTORY_RETENTION_TIME, Defaults.PREF_CLIPBOARD_HISTORY_RETENTION_TIME);
- mOneHandedModeEnabled = Settings.readOneHandedModeEnabled(prefs, isLandscape, mIsSplitKeyboardEnabled);
- mOneHandedModeGravity = Settings.readOneHandedModeGravity(prefs, isLandscape, mIsSplitKeyboardEnabled);
+ mOneHandedModeEnabled = Settings.readOneHandedModeEnabled(prefs, isLandscape);
+ mOneHandedModeGravity = Settings.readOneHandedModeGravity(prefs, isLandscape);
if (mOneHandedModeEnabled) {
final float baseScale = res.getFraction(R.fraction.config_one_handed_mode_width, 1, 1);
- final float extraScale = Settings.readOneHandedModeScale(prefs, isLandscape, mIsSplitKeyboardEnabled);
+ final float extraScale = Settings.readOneHandedModeScale(prefs, isLandscape);
mOneHandedModeScale = 1 - (1 - baseScale) * extraScale;
} else
mOneHandedModeScale = 1f;
@@ -273,7 +268,6 @@ public class SettingsValues {
mPopupKeyLabelSources = SubtypeUtilsKt.getPopupKeyLabelSources(selectedSubtype, prefs);
mAddToPersonalDictionary = prefs.getBoolean(Settings.PREF_ADD_TO_PERSONAL_DICTIONARY, Defaults.PREF_ADD_TO_PERSONAL_DICTIONARY);
mUseContactsDictionary = SettingsValues.readUseContactsEnabled(prefs, context);
- mUseAppsDictionary = prefs.getBoolean(Settings.PREF_USE_APPS, Defaults.PREF_USE_APPS);
mCustomNavBarColor = prefs.getBoolean(Settings.PREF_NAVBAR_COLOR, Defaults.PREF_NAVBAR_COLOR);
mNarrowKeyGaps = prefs.getBoolean(Settings.PREF_NARROW_KEY_GAPS, Defaults.PREF_NARROW_KEY_GAPS);
mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(
@@ -282,19 +276,19 @@ public class SettingsValues {
);
mSpacingAndPunctuations = new SpacingAndPunctuations(res, mUrlDetectionEnabled);
mBottomPaddingScale = Settings.readBottomPaddingScale(prefs, isLandscape);
- mSidePaddingScale = Settings.readSidePaddingScale(prefs, isLandscape, mIsSplitKeyboardEnabled);
+ mSidePaddingScale = Settings.readSidePaddingScale(prefs, isLandscape);
mLongPressSymbolsForNumpad = prefs.getBoolean(Settings.PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD, Defaults.PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD);
- mAutoShowToolbar = mToolbarMode == ToolbarMode.EXPANDABLE && prefs.getBoolean(Settings.PREF_AUTO_SHOW_TOOLBAR, Defaults.PREF_AUTO_SHOW_TOOLBAR);
- mAutoHideToolbar = mSuggestionsEnabledPerUserSettings && prefs.getBoolean(Settings.PREF_AUTO_HIDE_TOOLBAR, Defaults.PREF_AUTO_HIDE_TOOLBAR);
+ mAutoShowToolbar = prefs.getBoolean(Settings.PREF_AUTO_SHOW_TOOLBAR, Defaults.PREF_AUTO_SHOW_TOOLBAR);
+ mAutoHideToolbar = suggestionsEnabled && prefs.getBoolean(Settings.PREF_AUTO_HIDE_TOOLBAR, Defaults.PREF_AUTO_HIDE_TOOLBAR);
mAlphaAfterEmojiInEmojiView = prefs.getBoolean(Settings.PREF_ABC_AFTER_EMOJI, Defaults.PREF_ABC_AFTER_EMOJI);
mAlphaAfterClipHistoryEntry = prefs.getBoolean(Settings.PREF_ABC_AFTER_CLIP, Defaults.PREF_ABC_AFTER_CLIP);
mAlphaAfterSymbolAndSpace = prefs.getBoolean(Settings.PREF_ABC_AFTER_SYMBOL_SPACE, Defaults.PREF_ABC_AFTER_SYMBOL_SPACE);
mAlphaAfterNumpadAndSpace = prefs.getBoolean(Settings.PREF_ABC_AFTER_NUMPAD_SPACE, Defaults.PREF_ABC_AFTER_NUMPAD_SPACE);
mRemoveRedundantPopups = prefs.getBoolean(Settings.PREF_REMOVE_REDUNDANT_POPUPS, Defaults.PREF_REMOVE_REDUNDANT_POPUPS);
mSpaceBarText = prefs.getString(Settings.PREF_SPACE_BAR_TEXT, Defaults.PREF_SPACE_BAR_TEXT);
+ mEmojiMaxSdk = prefs.getInt(Settings.PREF_EMOJI_MAX_SDK, Defaults.PREF_EMOJI_MAX_SDK);
mFontSizeMultiplier = prefs.getFloat(Settings.PREF_FONT_SCALE, Defaults.PREF_FONT_SCALE);
mFontSizeMultiplierEmoji = prefs.getFloat(Settings.PREF_EMOJI_FONT_SCALE, Defaults.PREF_EMOJI_FONT_SCALE);
- mEmojiKeyFit = prefs.getBoolean(Settings.PREF_EMOJI_KEY_FIT, Defaults.PREF_EMOJI_KEY_FIT);
}
public boolean isApplicationSpecifiedCompletionsOn() {
@@ -358,12 +352,11 @@ public class SettingsValues {
return mDisplayOrientation == configuration.orientation;
}
- private static boolean readUseContactsEnabled(final SharedPreferences prefs, final Context ctx) {
+ private static boolean readUseContactsEnabled(final SharedPreferences prefs, final Context context) {
final boolean setting = prefs.getBoolean(Settings.PREF_USE_CONTACTS, Defaults.PREF_USE_CONTACTS);
if (!setting) return false;
- if (PermissionsUtil.checkAllPermissionsGranted(ctx, Manifest.permission.READ_CONTACTS)) {
+ if (PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS))
return true;
- }
// disable if permission not granted
prefs.edit().putBoolean(Settings.PREF_USE_CONTACTS, false).apply();
return false;
diff --git a/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java b/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java
index 73d66ad30..807561799 100644
--- a/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java
@@ -83,7 +83,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
final SharedPreferences prefs = KtxKt.prefs(this);
prefs.registerOnSharedPreferenceChangeListener(this);
onSharedPreferenceChanged(prefs, Settings.PREF_USE_CONTACTS);
- onSharedPreferenceChanged(prefs, Settings.PREF_USE_APPS);
final boolean blockOffensive = prefs.getBoolean(Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE, Defaults.PREF_BLOCK_POTENTIALLY_OFFENSIVE);
mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(blockOffensive, false);
}
@@ -94,19 +93,13 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
@Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
- if (key != null) switch (key) {
- case Settings.PREF_USE_CONTACTS -> {
+ if (Settings.PREF_USE_CONTACTS.equals(key)) {
final boolean useContactsDictionary = prefs.getBoolean(Settings.PREF_USE_CONTACTS, Defaults.PREF_USE_CONTACTS);
mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary);
- }
- case Settings.PREF_USE_APPS -> {
- final boolean useAppsDictionary = prefs.getBoolean(Settings.PREF_USE_APPS, Defaults.PREF_USE_APPS);
- mDictionaryFacilitatorCache.setUseAppsDictionary(useAppsDictionary);
- }
- case Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE -> {
+ } else if (Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE.equals(key)) {
final boolean blockOffensive = prefs.getBoolean(Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE, Defaults.PREF_BLOCK_POTENTIALLY_OFFENSIVE);
mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(blockOffensive, false);
- }}
+ }
}
@Override
diff --git a/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index 9f5ab79ec..94cd176d8 100644
--- a/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -335,8 +335,6 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
}
final WordComposer composer = new WordComposer();
- if (mLocale.getLanguage().equals("ko"))
- composer.restartCombining("hangul");
final int[] codePoints = StringUtils.toCodePointArray(text);
final int[] coordinates;
coordinates = keyboard.getCoordinates(codePoints);
diff --git a/app/src/main/java/helium314/keyboard/latin/suggestions/MoreSuggestions.java b/app/src/main/java/helium314/keyboard/latin/suggestions/MoreSuggestions.java
index 0d52736f5..2799c1f7f 100644
--- a/app/src/main/java/helium314/keyboard/latin/suggestions/MoreSuggestions.java
+++ b/app/src/main/java/helium314/keyboard/latin/suggestions/MoreSuggestions.java
@@ -168,12 +168,12 @@ public final class MoreSuggestions extends Keyboard {
}
public static final class Builder extends KeyboardBuilder {
- private final MoreSuggestionsView mPaneView;
+ private final PopupSuggestionsView mPaneView;
private SuggestedWords mSuggestedWords;
private int mFromIndex;
private int mToIndex;
- public Builder(final Context context, final MoreSuggestionsView paneView) {
+ public Builder(final Context context, final PopupSuggestionsView paneView) {
super(context, new MoreSuggestionsParam());
mPaneView = paneView;
}
diff --git a/app/src/main/java/helium314/keyboard/latin/suggestions/MoreSuggestionsView.kt b/app/src/main/java/helium314/keyboard/latin/suggestions/MoreSuggestionsView.kt
deleted file mode 100644
index 40c354291..000000000
--- a/app/src/main/java/helium314/keyboard/latin/suggestions/MoreSuggestionsView.kt
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- * modified
- * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
- */
-package helium314.keyboard.latin.suggestions
-
-import android.content.Context
-import android.util.AttributeSet
-import android.view.GestureDetector
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewGroup
-import helium314.keyboard.accessibility.AccessibilityUtils
-import helium314.keyboard.keyboard.Key
-import helium314.keyboard.keyboard.Keyboard
-import helium314.keyboard.keyboard.KeyboardActionListener
-import helium314.keyboard.keyboard.MainKeyboardView
-import helium314.keyboard.keyboard.PopupKeysKeyboardView
-import helium314.keyboard.keyboard.PopupKeysPanel
-import helium314.keyboard.latin.R
-import helium314.keyboard.latin.SuggestedWords
-import helium314.keyboard.latin.suggestions.MoreSuggestions.MoreSuggestionKey
-import helium314.keyboard.latin.utils.Log
-import kotlin.math.abs
-
-/**
- * A view that renders a virtual [MoreSuggestions]. It handles rendering of keys and detecting
- * key presses and touch movements.
- */
-class MoreSuggestionsView @JvmOverloads constructor(
- context: Context, attrs: AttributeSet?,
- defStyle: Int = R.attr.popupKeysKeyboardViewStyle
-) : PopupKeysKeyboardView(context, attrs, defStyle) {
-
- private val moreSuggestionsListener = object : KeyboardActionListener.Adapter() {
- override fun onCancelInput() {
- dismissPopupKeysPanel()
- }
- }
-
- private val moreSuggestionsController: PopupKeysPanel.Controller = object : PopupKeysPanel.Controller {
- override fun onDismissPopupKeysPanel() {
- mainKeyboardView.onDismissPopupKeysPanel()
- }
-
- override fun onShowPopupKeysPanel(panel: PopupKeysPanel) {
- mainKeyboardView.onShowPopupKeysPanel(panel)
- }
-
- override fun onCancelPopupKeysPanel() {
- dismissPopupKeysPanel()
- }
- }
-
- lateinit var listener: SuggestionStripView.Listener
- lateinit var mainKeyboardView: MainKeyboardView
-
- private val moreSuggestionsModalTolerance = context.resources.getDimensionPixelOffset(R.dimen.config_more_suggestions_modal_tolerance)
- private val moreSuggestionsBuilder by lazy { MoreSuggestions.Builder(context, this) }
-
- lateinit var gestureDetector: GestureDetector
- private var isInModalMode = false
-
- // Working variables for onInterceptTouchEvent(MotionEvent) and onTouchEvent(MotionEvent).
- private var needsToTransformTouchEventToHoverEvent = false
- private var isDispatchingHoverEventToMoreSuggestions = false
- private var lastX = 0
- private var lastY = 0
- private var originX = 0
- private var originY = 0
-
- // TODO: Remove redundant override method.
- override fun setKeyboard(keyboard: Keyboard) {
- super.setKeyboard(keyboard)
- isInModalMode = false
- // With accessibility mode off, mAccessibilityDelegate is set to null at the above PopupKeysKeyboardView#setKeyboard call.
- // With accessibility mode on, mAccessibilityDelegate is set to a PopupKeysKeyboardAccessibilityDelegate object at the above
- // PopupKeysKeyboardView#setKeyboard call.
- if (mAccessibilityDelegate != null) {
- mAccessibilityDelegate.setOpenAnnounce(R.string.spoken_open_more_suggestions)
- mAccessibilityDelegate.setCloseAnnounce(R.string.spoken_close_more_suggestions)
- }
- }
-
- override fun getDefaultCoordX() = (keyboard as MoreSuggestions).mOccupiedWidth / 2
-
- fun updateKeyboardGeometry(keyHeight: Int) {
- updateKeyDrawParams(keyHeight)
- }
-
- private fun setModalMode() {
- isInModalMode = true
- // Set vertical correction to zero (Reset popup keys keyboard sliding allowance R.dimen.config_popup_keys_keyboard_slide_allowance).
- mKeyDetector.setKeyboard(keyboard, -paddingLeft.toFloat(), -paddingTop.toFloat())
- }
-
- override fun onKeyInput(key: Key, x: Int, y: Int) {
- if (key !is MoreSuggestionKey) {
- Log.e(TAG, "Expected key is MoreSuggestionKey, but found ${key.javaClass.name}")
- return
- }
- val keyboard = keyboard
- if (keyboard !is MoreSuggestions) {
- Log.e(TAG, "Expected keyboard is MoreSuggestions, but found ${keyboard?.javaClass?.name}")
- return
- }
- val suggestedWords = keyboard.mSuggestedWords
- val index = key.mSuggestedWordIndex
- if (index < 0 || index >= suggestedWords.size()) {
- Log.e(TAG, "Selected suggestion has an illegal index: $index")
- return
- }
- listener.pickSuggestionManually(suggestedWords.getInfo(index))
- dismissPopupKeysPanel()
- }
-
- internal fun show(
- suggestedWords: SuggestedWords, fromIndex: Int, container: View,
- layoutHelper: SuggestionStripLayoutHelper, parentView: View
- ): Boolean {
- val maxWidth = parentView.width - container.paddingLeft - container.paddingRight
- val parentKeyboard = mainKeyboardView.keyboard ?: return false
- val keyboard = moreSuggestionsBuilder.layout(
- suggestedWords, fromIndex, maxWidth,
- (maxWidth * layoutHelper.mMinMoreSuggestionsWidth).toInt(),
- layoutHelper.maxMoreSuggestionsRow, parentKeyboard
- ).build()
- setKeyboard(keyboard)
- container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
-
- val pointX = parentView.width / 2
- val pointY = -layoutHelper.mMoreSuggestionsBottomGap
- showPopupKeysPanel(parentView, moreSuggestionsController, pointX, pointY, moreSuggestionsListener)
- originX = lastX
- originY = lastY
- return true
- }
-
- fun shouldInterceptTouchEvent(motionEvent: MotionEvent): Boolean {
- if (!isShowingInParent) {
- lastX = motionEvent.x.toInt()
- lastY = motionEvent.y.toInt()
- return gestureDetector.onTouchEvent(motionEvent)
- }
- if (isInModalMode) {
- return false
- }
-
- val index = motionEvent.actionIndex
- if (abs((motionEvent.getX(index).toInt() - originX).toDouble()) >= moreSuggestionsModalTolerance
- || originY - motionEvent.getY(index).toInt() >= moreSuggestionsModalTolerance
- ) {
- // Decided to be in the sliding suggestion mode only when the touch point has been moved
- // upward. Further MotionEvents will be delivered to SuggestionStripView.onTouchEvent.
- needsToTransformTouchEventToHoverEvent = AccessibilityUtils.instance.isTouchExplorationEnabled
- isDispatchingHoverEventToMoreSuggestions = false
- return true
- }
-
- if (motionEvent.action == MotionEvent.ACTION_UP || motionEvent.action == MotionEvent.ACTION_POINTER_UP) {
- // Decided to be in the modal input mode.
- setModalMode()
- }
- return false
- }
-
- fun touchEvent(motionEvent: MotionEvent) {
- if (!isShowingInParent) {
- return // Ignore any touch event while more suggestions panel hasn't been shown.
- }
- // In the sliding input mode. MotionEvent should be forwarded to MoreSuggestionsView.
- val index = motionEvent.actionIndex
- val x = translateX(motionEvent.getX(index).toInt())
- val y = translateY(motionEvent.getY(index).toInt())
- motionEvent.setLocation(x.toFloat(), y.toFloat())
- if (!needsToTransformTouchEventToHoverEvent) {
- onTouchEvent(motionEvent)
- return
- }
- // In sliding suggestion mode with accessibility mode on, a touch event should be transformed to a hover event.
- val onMoreSuggestions = x in 0..= suggestedWords.size()) {
+ Log.e(TAG, "Selected suggestion has an illegal index: " + index);
+ return;
+ }
+ if (!(mListener instanceof MoreSuggestionsListener)) {
+ Log.e(TAG, "Expected mListener is MoreSuggestionsListener, but found "
+ + mListener.getClass().getName());
+ return;
+ }
+ ((MoreSuggestionsListener)mListener).onSuggestionSelected(suggestedWords.getInfo(index));
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripLayoutHelper.java b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripLayoutHelper.java
index e7cf2ed56..07535c641 100644
--- a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -373,7 +373,7 @@ final class SuggestionStripLayoutHelper {
x += wordView.getMeasuredWidth();
if (SuggestionStripView.DEBUG_SUGGESTIONS) {
- layoutDebugInfo(positionInStrip, placerView, (int) stripView.getX() + x);
+ layoutDebugInfo(positionInStrip, placerView, x);
}
}
return startIndexOfMoreSuggestions;
@@ -436,7 +436,8 @@ final class SuggestionStripLayoutHelper {
placerView.addView(debugInfoView);
debugInfoView.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int infoWidth = debugInfoView.getMeasuredWidth();
- ViewLayoutUtils.placeViewAt(debugInfoView, x - infoWidth, 0, infoWidth, debugInfoView.getMeasuredHeight());
+ final int y = debugInfoView.getMeasuredHeight();
+ ViewLayoutUtils.placeViewAt(debugInfoView, x - infoWidth, y, infoWidth, debugInfoView.getMeasuredHeight());
}
private int getSuggestionWidth(final int positionInStrip, final int maxWidth) {
diff --git a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.java b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.java
new file mode 100644
index 000000000..860759831
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.java
@@ -0,0 +1,735 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.latin.suggestions;
+
+import static helium314.keyboard.latin.utils.ToolbarUtilsKt.*;
+
+import android.annotation.SuppressLint;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.OvalShape;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.GestureDetector;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import helium314.keyboard.accessibility.AccessibilityUtils;
+import helium314.keyboard.keyboard.Keyboard;
+import helium314.keyboard.keyboard.KeyboardSwitcher;
+import helium314.keyboard.keyboard.MainKeyboardView;
+import helium314.keyboard.keyboard.PopupKeysPanel;
+import helium314.keyboard.keyboard.internal.KeyboardIconsSet;
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
+import helium314.keyboard.latin.AudioAndHapticFeedbackManager;
+import helium314.keyboard.latin.Dictionary;
+import helium314.keyboard.latin.R;
+import helium314.keyboard.latin.SuggestedWords;
+import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
+import helium314.keyboard.latin.common.ColorType;
+import helium314.keyboard.latin.common.Colors;
+import helium314.keyboard.latin.common.Constants;
+import helium314.keyboard.latin.define.DebugFlags;
+import helium314.keyboard.latin.settings.DebugSettings;
+import helium314.keyboard.latin.settings.Defaults;
+import helium314.keyboard.latin.settings.Settings;
+import helium314.keyboard.latin.settings.SettingsValues;
+import helium314.keyboard.latin.suggestions.PopupSuggestionsView.MoreSuggestionsListener;
+import helium314.keyboard.latin.utils.KtxKt;
+import helium314.keyboard.latin.utils.Log;
+import helium314.keyboard.latin.utils.ToolbarKey;
+import helium314.keyboard.latin.utils.ToolbarUtilsKt;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public final class SuggestionStripView extends RelativeLayout implements OnClickListener,
+ OnLongClickListener, SharedPreferences.OnSharedPreferenceChangeListener {
+ public interface Listener {
+ void pickSuggestionManually(SuggestedWordInfo word);
+ void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat);
+ void removeSuggestion(final String word);
+ }
+
+ public static boolean DEBUG_SUGGESTIONS;
+ private static final float DEBUG_INFO_TEXT_SIZE_IN_DIP = 6.5f;
+ private static final String TAG = SuggestionStripView.class.getSimpleName();
+
+ private final ViewGroup mSuggestionsStrip;
+ private final ImageButton mToolbarExpandKey;
+ private final Drawable mIncognitoIcon;
+ private final Drawable mToolbarArrowIcon;
+ private final Drawable mBinIcon;
+ private final ViewGroup mToolbar;
+ private final View mToolbarContainer;
+ private final ViewGroup mPinnedKeys;
+ private final GradientDrawable mEnabledToolKeyBackground = new GradientDrawable();
+ private final Drawable mDefaultBackground;
+ MainKeyboardView mMainKeyboardView;
+
+ private final View mMoreSuggestionsContainer;
+ private final PopupSuggestionsView mMoreSuggestionsView;
+ private final MoreSuggestions.Builder mMoreSuggestionsBuilder;
+
+ private final ArrayList mWordViews = new ArrayList<>();
+ private final ArrayList mDebugInfoViews = new ArrayList<>();
+ private final ArrayList mDividerViews = new ArrayList<>();
+
+ Listener mListener;
+ private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance();
+ private int mStartIndexOfMoreSuggestions;
+ private int mRtl = 1; // 1 if LTR, -1 if RTL
+
+ private final SuggestionStripLayoutHelper mLayoutHelper;
+ private final StripVisibilityGroup mStripVisibilityGroup;
+ private boolean isExternalSuggestionVisible = false; // Required to disable the more suggestions if other suggestions are visible
+
+ private static class StripVisibilityGroup {
+ private final View mSuggestionStripView;
+ private final View mSuggestionsStrip;
+
+ public StripVisibilityGroup(final View suggestionStripView,
+ final ViewGroup suggestionsStrip) {
+ mSuggestionStripView = suggestionStripView;
+ mSuggestionsStrip = suggestionsStrip;
+ showSuggestionsStrip();
+ }
+
+ public void setLayoutDirection(final int layoutDirection) {
+ mSuggestionStripView.setLayoutDirection(layoutDirection);
+ mSuggestionsStrip.setLayoutDirection(layoutDirection);
+ }
+
+ public void showSuggestionsStrip() {
+ mSuggestionsStrip.setVisibility(VISIBLE);
+ }
+
+ }
+
+ /**
+ * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user.
+ */
+ public SuggestionStripView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, R.attr.suggestionStripViewStyle);
+ }
+
+ @SuppressLint("InflateParams") // does not seem suitable here
+ public SuggestionStripView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ final Colors colors = Settings.getValues().mColors;
+ final SharedPreferences prefs = KtxKt.prefs(context);
+ DEBUG_SUGGESTIONS = prefs.getBoolean(DebugSettings.PREF_SHOW_SUGGESTION_INFOS, Defaults.PREF_SHOW_SUGGESTION_INFOS);
+
+ final LayoutInflater inflater = LayoutInflater.from(context);
+ inflater.inflate(R.layout.suggestions_strip, this);
+
+ mSuggestionsStrip = findViewById(R.id.suggestions_strip);
+ mToolbarExpandKey = findViewById(R.id.suggestions_strip_toolbar_key);
+ mStripVisibilityGroup = new StripVisibilityGroup(this, mSuggestionsStrip);
+ mPinnedKeys = findViewById(R.id.pinned_keys);
+ mToolbar = findViewById(R.id.toolbar);
+ mToolbarContainer = findViewById(R.id.toolbar_container);
+
+ final Typeface customTypeface = Settings.getInstance().getCustomTypeface();
+ for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) {
+ final TextView word = new TextView(context, null, R.attr.suggestionWordStyle);
+ word.setContentDescription(getResources().getString(R.string.spoken_empty_suggestion));
+ word.setOnClickListener(this);
+ word.setOnLongClickListener(this);
+ if (customTypeface != null)
+ word.setTypeface(customTypeface);
+ colors.setBackground(word, ColorType.STRIP_BACKGROUND);
+ mWordViews.add(word);
+ final View divider = inflater.inflate(R.layout.suggestion_divider, null);
+ mDividerViews.add(divider);
+ final TextView info = new TextView(context, null, R.attr.suggestionWordStyle);
+ info.setTextColor(colors.get(ColorType.KEY_TEXT));
+ info.setTextSize(TypedValue.COMPLEX_UNIT_DIP, DEBUG_INFO_TEXT_SIZE_IN_DIP);
+ mDebugInfoViews.add(info);
+ }
+
+ mLayoutHelper = new SuggestionStripLayoutHelper(context, attrs, defStyle, mWordViews, mDividerViews, mDebugInfoViews);
+
+ mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
+ mMoreSuggestionsView = mMoreSuggestionsContainer.findViewById(R.id.more_suggestions_view);
+ mMoreSuggestionsBuilder = new MoreSuggestions.Builder(context, mMoreSuggestionsView);
+
+ final Resources res = context.getResources();
+ mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset(
+ R.dimen.config_more_suggestions_modal_tolerance);
+ mMoreSuggestionsSlidingDetector = new GestureDetector(context, mMoreSuggestionsSlidingListener);
+
+ final KeyboardIconsSet iconsSet = KeyboardIconsSet.Companion.getInstance();
+ mIncognitoIcon = iconsSet.getNewDrawable(ToolbarKey.INCOGNITO.name(), context);
+ mToolbarArrowIcon = iconsSet.getNewDrawable(KeyboardIconsSet.NAME_TOOLBAR_KEY, context);
+ mBinIcon = iconsSet.getNewDrawable(KeyboardIconsSet.NAME_BIN, context);
+
+ final LinearLayout.LayoutParams toolbarKeyLayoutParams = new LinearLayout.LayoutParams(
+ getResources().getDimensionPixelSize(R.dimen.config_suggestions_strip_edge_key_width),
+ LinearLayout.LayoutParams.MATCH_PARENT
+ );
+ for (final ToolbarKey key : ToolbarUtilsKt.getEnabledToolbarKeys(prefs)) {
+ final ImageButton button = createToolbarKey(context, iconsSet, key);
+ button.setLayoutParams(toolbarKeyLayoutParams);
+ setupKey(button, colors);
+ mToolbar.addView(button);
+ }
+
+ final int toolbarHeight = Math.min(mToolbarExpandKey.getLayoutParams().height, (int) getResources().getDimension(R.dimen.config_suggestions_strip_height));
+ mToolbarExpandKey.getLayoutParams().height = toolbarHeight;
+ mToolbarExpandKey.getLayoutParams().width = toolbarHeight; // we want it square
+ colors.setBackground(mToolbarExpandKey, ColorType.STRIP_BACKGROUND);
+ mDefaultBackground = mToolbarExpandKey.getBackground();
+ mEnabledToolKeyBackground.setColors(new int[] {colors.get(ColorType.TOOL_BAR_KEY_ENABLED_BACKGROUND) | 0xFF000000, Color.TRANSPARENT}); // ignore alpha on accent color
+ mEnabledToolKeyBackground.setGradientType(GradientDrawable.RADIAL_GRADIENT);
+ mEnabledToolKeyBackground.setGradientRadius(mToolbarExpandKey.getLayoutParams().height / 2f); // nothing else has a usable height at this state
+
+ mToolbarExpandKey.setOnClickListener(this);
+ mToolbarExpandKey.setImageDrawable(Settings.getValues().mIncognitoModeEnabled ? mIncognitoIcon : mToolbarArrowIcon);
+ colors.setColor(mToolbarExpandKey, ColorType.TOOL_BAR_EXPAND_KEY);
+ mToolbarExpandKey.setBackground(new ShapeDrawable(new OvalShape())); // ShapeDrawable color is black, need src_atop filter
+ mToolbarExpandKey.getBackground().setColorFilter(colors.get(ColorType.TOOL_BAR_EXPAND_KEY_BACKGROUND), PorterDuff.Mode.SRC_ATOP);
+ mToolbarExpandKey.getLayoutParams().height *= 0.82; // shrink the whole key a little (drawable not affected)
+ mToolbarExpandKey.getLayoutParams().width *= 0.82;
+
+ for (final ToolbarKey pinnedKey : ToolbarUtilsKt.getPinnedToolbarKeys(prefs)) {
+ final ImageButton button = createToolbarKey(context, iconsSet, pinnedKey);
+ button.setLayoutParams(toolbarKeyLayoutParams);
+ setupKey(button, colors);
+ mPinnedKeys.addView(button);
+ final View pinnedKeyInToolbar = mToolbar.findViewWithTag(pinnedKey);
+ if (pinnedKeyInToolbar != null && Settings.getValues().mQuickPinToolbarKeys)
+ pinnedKeyInToolbar.setBackground(mEnabledToolKeyBackground);
+ }
+
+ colors.setBackground(this, ColorType.STRIP_BACKGROUND);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, @Nullable String key) {
+ ToolbarUtilsKt.setToolbarButtonsActivatedStateOnPrefChange(mPinnedKeys, key);
+ ToolbarUtilsKt.setToolbarButtonsActivatedStateOnPrefChange(mToolbar, key);
+ }
+
+ /**
+ * A connection back to the input method.
+ */
+ public void setListener(final Listener listener, final View inputView) {
+ mListener = listener;
+ mMainKeyboardView = inputView.findViewById(R.id.keyboard_view);
+ }
+
+ private void updateKeys() {
+ final SettingsValues currentSettingsValues = Settings.getValues();
+ final View toolbarVoiceKey = mToolbar.findViewWithTag(ToolbarKey.VOICE);
+ if (toolbarVoiceKey != null)
+ toolbarVoiceKey.setVisibility(currentSettingsValues.mShowsVoiceInputKey ? VISIBLE : GONE);
+ final View pinnedVoiceKey = mPinnedKeys.findViewWithTag(ToolbarKey.VOICE);
+ if (pinnedVoiceKey != null)
+ pinnedVoiceKey.setVisibility(currentSettingsValues.mShowsVoiceInputKey ? VISIBLE : GONE);
+ mToolbarExpandKey.setImageDrawable(currentSettingsValues.mIncognitoModeEnabled ? mIncognitoIcon : mToolbarArrowIcon);
+ mToolbarExpandKey.setScaleX((mToolbarContainer.getVisibility() != VISIBLE ? 1f : -1f) * mRtl);
+
+ // hide pinned keys if device is locked, and avoid expanding toolbar
+ final KeyguardManager km = (KeyguardManager) getContext().getSystemService(Context.KEYGUARD_SERVICE);
+ final boolean hideToolbarKeys = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1
+ ? km.isDeviceLocked()
+ : km.isKeyguardLocked();
+ mToolbarExpandKey.setOnClickListener(hideToolbarKeys ? null : this);
+ mPinnedKeys.setVisibility(hideToolbarKeys ? GONE : mSuggestionsStrip.getVisibility());
+ isExternalSuggestionVisible = false;
+ }
+
+ public void setRtl(final boolean isRtlLanguage) {
+ final int layoutDirection;
+ if (!Settings.getValues().mVarToolbarDirection)
+ layoutDirection = View.LAYOUT_DIRECTION_LOCALE;
+ else{
+ layoutDirection = isRtlLanguage ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR;
+ mRtl = isRtlLanguage ? -1 : 1;
+ }
+ mStripVisibilityGroup.setLayoutDirection(layoutDirection);
+ }
+
+ public void setSuggestions(final SuggestedWords suggestedWords, final boolean isRtlLanguage) {
+ clear();
+ setRtl(isRtlLanguage);
+ updateKeys();
+ mSuggestedWords = suggestedWords;
+ mStartIndexOfMoreSuggestions = mLayoutHelper.layoutAndReturnStartIndexOfMoreSuggestions(
+ getContext(), mSuggestedWords, mSuggestionsStrip, this);
+ }
+
+ public void setExternalSuggestionView(final View view) {
+ clear();
+ isExternalSuggestionVisible = true;
+ mSuggestionsStrip.addView(view);
+ if (Settings.getValues().mAutoHideToolbar)
+ setToolbarVisibility(false);
+ }
+
+ @Override
+ public void onVisibilityChanged(@NonNull final View view, final int visibility) {
+ super.onVisibilityChanged(view, visibility);
+ if (view == this)
+ // workaround for a bug with inline suggestions views that just keep showing up otherwise, https://github.com/Helium314/HeliBoard/pull/386
+ mSuggestionsStrip.setVisibility(visibility);
+ }
+
+ public void setMoreSuggestionsHeight(final int remainingHeight) {
+ mLayoutHelper.setMoreSuggestionsHeight(remainingHeight);
+ }
+
+ @SuppressLint("ClickableViewAccessibility") // why would "null" need to call View#performClick?
+ private void clear() {
+ mSuggestionsStrip.removeAllViews();
+ if (DEBUG_SUGGESTIONS)
+ removeAllDebugInfoViews();
+ if (mToolbarContainer.getVisibility() != VISIBLE)
+ mStripVisibilityGroup.showSuggestionsStrip();
+ dismissMoreSuggestionsPanel();
+ for (final TextView word : mWordViews) {
+ word.setOnTouchListener(null);
+ }
+ }
+
+ private void removeAllDebugInfoViews() {
+ // The debug info views may be placed as children views of this {@link SuggestionStripView}.
+ for (final View debugInfoView : mDebugInfoViews) {
+ final ViewParent parent = debugInfoView.getParent();
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup)parent).removeView(debugInfoView);
+ }
+ }
+ }
+
+ private final MoreSuggestionsListener mMoreSuggestionsListener = new MoreSuggestionsListener() {
+ @Override
+ public void onSuggestionSelected(final SuggestedWordInfo wordInfo) {
+ mListener.pickSuggestionManually(wordInfo);
+ dismissMoreSuggestionsPanel();
+ }
+
+ @Override
+ public void onCancelInput() {
+ dismissMoreSuggestionsPanel();
+ }
+ };
+
+ private final PopupKeysPanel.Controller mMoreSuggestionsController =
+ new PopupKeysPanel.Controller() {
+ @Override
+ public void onDismissPopupKeysPanel() {
+ mMainKeyboardView.onDismissPopupKeysPanel();
+ }
+
+ @Override
+ public void onShowPopupKeysPanel(final PopupKeysPanel panel) {
+ mMainKeyboardView.onShowPopupKeysPanel(panel);
+ }
+
+ @Override
+ public void onCancelPopupKeysPanel() {
+ dismissMoreSuggestionsPanel();
+ }
+ };
+
+ public boolean isShowingMoreSuggestionPanel() {
+ return mMoreSuggestionsView.isShowingInParent();
+ }
+
+ public void dismissMoreSuggestionsPanel() {
+ mMoreSuggestionsView.dismissPopupKeysPanel();
+ }
+
+ @Override
+ public boolean onLongClick(final View view) {
+ AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(Constants.NOT_A_CODE, this);
+ if (view.getTag() instanceof ToolbarKey) {
+ onLongClickToolKey(view);
+ return true;
+ }
+ if (view instanceof TextView && mWordViews.contains(view)) {
+ return onLongClickSuggestion((TextView) view);
+ } else return showMoreSuggestions();
+ }
+
+ private void onLongClickToolKey(final View view) {
+ if (!(view.getTag() instanceof ToolbarKey tag)) return;
+ if (view.getParent() == mPinnedKeys || !Settings.getValues().mQuickPinToolbarKeys) {
+ final int longClickCode = getCodeForToolbarKeyLongClick(tag);
+ if (longClickCode != KeyCode.UNSPECIFIED) {
+ mListener.onCodeInput(longClickCode, Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, false);
+ }
+ } else if (view.getParent() == mToolbar) {
+ final View pinnedKeyView = mPinnedKeys.findViewWithTag(tag);
+ if (pinnedKeyView == null) {
+ addKeyToPinnedKeys(tag);
+ mToolbar.findViewWithTag(tag).setBackground(mEnabledToolKeyBackground);
+ ToolbarUtilsKt.addPinnedKey(KtxKt.prefs(getContext()), tag);
+ } else {
+ ToolbarUtilsKt.removePinnedKey(KtxKt.prefs(getContext()), tag);
+ mToolbar.findViewWithTag(tag).setBackground(mDefaultBackground.getConstantState().newDrawable(getResources()));
+ mPinnedKeys.removeView(pinnedKeyView);
+ }
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility") // no need for View#performClick, we return false mostly anyway
+ private boolean onLongClickSuggestion(final TextView wordView) {
+ boolean showIcon = true;
+ if (wordView.getTag() instanceof Integer) {
+ final int index = (int) wordView.getTag();
+ if (index < mSuggestedWords.size() && mSuggestedWords.getInfo(index).mSourceDict == Dictionary.DICTIONARY_USER_TYPED)
+ showIcon = false;
+ }
+ if (showIcon) {
+ final Drawable icon = mBinIcon;
+ Settings.getValues().mColors.setColor(icon, ColorType.REMOVE_SUGGESTION_ICON);
+ int w = icon.getIntrinsicWidth();
+ int h = icon.getIntrinsicWidth();
+ wordView.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
+ wordView.setEllipsize(TextUtils.TruncateAt.END);
+ AtomicBoolean downOk = new AtomicBoolean(false);
+ wordView.setOnTouchListener((view1, motionEvent) -> {
+ if (motionEvent.getAction() == MotionEvent.ACTION_UP && downOk.get()) {
+ final float x = motionEvent.getX();
+ final float y = motionEvent.getY();
+ if (0 < x && x < w && 0 < y && y < h) {
+ removeSuggestion(wordView);
+ wordView.cancelLongPress();
+ wordView.setPressed(false);
+ return true;
+ }
+ } else if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
+ final float x = motionEvent.getX();
+ final float y = motionEvent.getY();
+ if (0 < x && x < w && 0 < y && y < h) {
+ downOk.set(true);
+ }
+ }
+ return false;
+ });
+ }
+ if (DebugFlags.DEBUG_ENABLED && (isShowingMoreSuggestionPanel() || !showMoreSuggestions())) {
+ showSourceDict(wordView);
+ return true;
+ } else return showMoreSuggestions();
+ }
+
+ private void showSourceDict(final TextView wordView) {
+ final String word = wordView.getText().toString();
+ final int index;
+ if (wordView.getTag() instanceof Integer) {
+ index = (int) wordView.getTag();
+ } else return;
+ if (index >= mSuggestedWords.size()) return;
+ final SuggestedWordInfo info = mSuggestedWords.getInfo(index);
+ if (!info.getWord().equals(word)) return;
+ final String text = info.mSourceDict.mDictType + ":" + info.mSourceDict.mLocale;
+ if (isShowingMoreSuggestionPanel()) {
+ mMoreSuggestionsView.dismissPopupKeysPanel();
+ }
+ KeyboardSwitcher.getInstance().showToast(text, true);
+ }
+
+ private void removeSuggestion(TextView wordView) {
+ final String word = wordView.getText().toString();
+ mListener.removeSuggestion(word);
+ mMoreSuggestionsView.dismissPopupKeysPanel();
+ // show suggestions, but without the removed word
+ final ArrayList sw = new ArrayList<>();
+ for (int i = 0; i < mSuggestedWords.size(); i ++) {
+ final SuggestedWordInfo info = mSuggestedWords.getInfo(i);
+ if (!info.getWord().equals(word))
+ sw.add(info);
+ }
+ ArrayList rs = null;
+ if (mSuggestedWords.mRawSuggestions != null) {
+ rs = mSuggestedWords.mRawSuggestions;
+ for (int i = 0; i < rs.size(); i ++) {
+ if (rs.get(i).getWord().equals(word)) {
+ rs.remove(i);
+ break;
+ }
+ }
+ }
+ // copied code from setSuggestions, but without the Rtl part
+ clear();
+ mSuggestedWords = new SuggestedWords(sw, rs, mSuggestedWords.getTypedWordInfo(),
+ mSuggestedWords.mTypedWordValid, mSuggestedWords.mWillAutoCorrect,
+ mSuggestedWords.mIsObsoleteSuggestions, mSuggestedWords.mInputStyle,
+ mSuggestedWords.mSequenceNumber);
+ mStartIndexOfMoreSuggestions = mLayoutHelper.layoutAndReturnStartIndexOfMoreSuggestions(
+ getContext(), mSuggestedWords, mSuggestionsStrip, SuggestionStripView.this);
+ mStripVisibilityGroup.showSuggestionsStrip();
+ // Show the toolbar if no suggestions are left and the "Auto show toolbar" setting is enabled
+ if (mSuggestedWords.isEmpty() && Settings.getValues().mAutoShowToolbar){
+ setToolbarVisibility(true);
+ }
+ }
+
+ boolean showMoreSuggestions() {
+ final Keyboard parentKeyboard = mMainKeyboardView.getKeyboard();
+ if (parentKeyboard == null) {
+ return false;
+ }
+ final SuggestionStripLayoutHelper layoutHelper = mLayoutHelper;
+ if (mSuggestedWords.size() <= mStartIndexOfMoreSuggestions) {
+ return false;
+ }
+ final int stripWidth = getWidth();
+ final View container = mMoreSuggestionsContainer;
+ final int maxWidth = stripWidth - container.getPaddingLeft() - container.getPaddingRight();
+ final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder;
+ builder.layout(mSuggestedWords, mStartIndexOfMoreSuggestions, maxWidth,
+ (int)(maxWidth * layoutHelper.mMinMoreSuggestionsWidth),
+ layoutHelper.getMaxMoreSuggestionsRow(), parentKeyboard);
+ mMoreSuggestionsView.setKeyboard(builder.build());
+ container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ final int pointX = stripWidth / 2;
+ final int pointY = -layoutHelper.mMoreSuggestionsBottomGap;
+ mMoreSuggestionsView.showPopupKeysPanel(this, mMoreSuggestionsController, pointX, pointY,
+ mMoreSuggestionsListener);
+ mOriginX = mLastX;
+ mOriginY = mLastY;
+ for (int i = 0; i < mStartIndexOfMoreSuggestions; i++) {
+ mWordViews.get(i).setPressed(false);
+ }
+ return true;
+ }
+
+ // Working variables for {@link onInterceptTouchEvent(MotionEvent)} and
+ // {@link onTouchEvent(MotionEvent)}.
+ private int mLastX;
+ private int mLastY;
+ private int mOriginX;
+ private int mOriginY;
+ private final int mMoreSuggestionsModalTolerance;
+ private boolean mNeedsToTransformTouchEventToHoverEvent;
+ private boolean mIsDispatchingHoverEventToMoreSuggestions;
+ private final GestureDetector mMoreSuggestionsSlidingDetector;
+ private final GestureDetector.OnGestureListener mMoreSuggestionsSlidingListener =
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onScroll(@Nullable MotionEvent down, @NonNull MotionEvent me, float deltaX, float deltaY) {
+ if (down == null) return false;
+ final float dy = me.getY() - down.getY();
+ if (mToolbarContainer.getVisibility() != VISIBLE && deltaY > 0 && dy < 0) {
+ return showMoreSuggestions();
+ }
+ return false;
+ }
+ };
+
+ @Override
+ public boolean onInterceptTouchEvent(final MotionEvent me) {
+
+ // Disable More Suggestions if inline autofill suggestions is visible
+ if(isExternalSuggestionVisible) {
+ return false;
+ }
+
+ // Detecting sliding up finger to show {@link MoreSuggestionsView}.
+ if (!mMoreSuggestionsView.isShowingInParent()) {
+ mLastX = (int)me.getX();
+ mLastY = (int)me.getY();
+ return mMoreSuggestionsSlidingDetector.onTouchEvent(me);
+ }
+ if (mMoreSuggestionsView.isInModalMode()) {
+ return false;
+ }
+
+ final int action = me.getAction();
+ final int index = me.getActionIndex();
+ final int x = (int)me.getX(index);
+ final int y = (int)me.getY(index);
+ if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance
+ || mOriginY - y >= mMoreSuggestionsModalTolerance) {
+ // Decided to be in the sliding suggestion mode only when the touch point has been moved
+ // upward. Further {@link MotionEvent}s will be delivered to
+ // {@link #onTouchEvent(MotionEvent)}.
+ mNeedsToTransformTouchEventToHoverEvent =
+ AccessibilityUtils.Companion.getInstance().isTouchExplorationEnabled();
+ mIsDispatchingHoverEventToMoreSuggestions = false;
+ return true;
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
+ // Decided to be in the modal input mode.
+ mMoreSuggestionsView.setModalMode();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(final AccessibilityEvent event) {
+ // Don't populate accessibility event with suggested words and voice key.
+ return true;
+ }
+
+ @Override
+ @SuppressLint("ClickableViewAccessibility") // ok, perform click again, but why?
+ public boolean onTouchEvent(final MotionEvent me) {
+ if (!mMoreSuggestionsView.isShowingInParent()) {
+ // Ignore any touch event while more suggestions panel hasn't been shown.
+ // Detecting sliding up is done at {@link #onInterceptTouchEvent}.
+ return true;
+ }
+ // In the sliding input mode. {@link MotionEvent} should be forwarded to
+ // {@link MoreSuggestionsView}.
+ final int index = me.getActionIndex();
+ final int x = mMoreSuggestionsView.translateX((int)me.getX(index));
+ final int y = mMoreSuggestionsView.translateY((int)me.getY(index));
+ me.setLocation(x, y);
+ if (!mNeedsToTransformTouchEventToHoverEvent) {
+ mMoreSuggestionsView.onTouchEvent(me);
+ return true;
+ }
+ // In sliding suggestion mode with accessibility mode on, a touch event should be
+ // transformed to a hover event.
+ final int width = mMoreSuggestionsView.getWidth();
+ final int height = mMoreSuggestionsView.getHeight();
+ final boolean onMoreSuggestions = (x >= 0 && x < width && y >= 0 && y < height);
+ if (!onMoreSuggestions && !mIsDispatchingHoverEventToMoreSuggestions) {
+ // Just drop this touch event because dispatching hover event isn't started yet and
+ // the touch event isn't on {@link MoreSuggestionsView}.
+ return true;
+ }
+ final int hoverAction;
+ if (onMoreSuggestions && !mIsDispatchingHoverEventToMoreSuggestions) {
+ // Transform this touch event to a hover enter event and start dispatching a hover
+ // event to {@link MoreSuggestionsView}.
+ mIsDispatchingHoverEventToMoreSuggestions = true;
+ hoverAction = MotionEvent.ACTION_HOVER_ENTER;
+ } else if (me.getActionMasked() == MotionEvent.ACTION_UP) {
+ // Transform this touch event to a hover exit event and stop dispatching a hover event
+ // after this.
+ mIsDispatchingHoverEventToMoreSuggestions = false;
+ mNeedsToTransformTouchEventToHoverEvent = false;
+ hoverAction = MotionEvent.ACTION_HOVER_EXIT;
+ } else {
+ // Transform this touch event to a hover move event.
+ hoverAction = MotionEvent.ACTION_HOVER_MOVE;
+ }
+ me.setAction(hoverAction);
+ mMoreSuggestionsView.onHoverEvent(me);
+ return true;
+ }
+
+ @Override
+ public void onClick(final View view) {
+ AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(KeyCode.NOT_SPECIFIED, this);
+ final Object tag = view.getTag();
+ if (tag instanceof ToolbarKey) {
+ final int code = getCodeForToolbarKey((ToolbarKey) tag);
+ if (code != KeyCode.UNSPECIFIED) {
+ Log.d(TAG, "click toolbar key "+tag);
+ mListener.onCodeInput(code, Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, false);
+ if (tag == ToolbarKey.INCOGNITO)
+ updateKeys(); // update expand key icon
+ return;
+ }
+ }
+ if (view == mToolbarExpandKey) {
+ setToolbarVisibility(mToolbarContainer.getVisibility() != VISIBLE);
+ }
+
+
+ // {@link Integer} tag is set at
+ // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and
+ // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup}
+ if (tag instanceof Integer) {
+ final int index = (Integer) tag;
+ if (index >= mSuggestedWords.size()) {
+ return;
+ }
+ final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index);
+ mListener.pickSuggestionManually(wordInfo);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ dismissMoreSuggestionsPanel();
+ }
+
+ @Override
+ protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
+ // Called by the framework when the size is known. Show the important notice if applicable.
+ // This may be overriden by showing suggestions later, if applicable.
+ }
+
+ public void setToolbarVisibility(final boolean visible) {
+ final KeyguardManager km = (KeyguardManager) getContext().getSystemService(Context.KEYGUARD_SERVICE);
+ final boolean locked = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1
+ ? km.isDeviceLocked()
+ : km.isKeyguardLocked();
+ if (locked) {
+ mPinnedKeys.setVisibility(GONE);
+ mSuggestionsStrip.setVisibility(VISIBLE);
+ mToolbarContainer.setVisibility(GONE);
+ } else if (visible) {
+ mPinnedKeys.setVisibility(GONE);
+ mSuggestionsStrip.setVisibility(GONE);
+ mToolbarContainer.setVisibility(VISIBLE);
+ } else {
+ mToolbarContainer.setVisibility(GONE);
+ mSuggestionsStrip.setVisibility(VISIBLE);
+ mPinnedKeys.setVisibility(VISIBLE);
+ }
+ mToolbarExpandKey.setScaleX((visible && !locked ? -1f : 1f) * mRtl);
+ }
+
+ private void addKeyToPinnedKeys(final ToolbarKey pinnedKey) {
+ final ImageButton original = mToolbar.findViewWithTag(pinnedKey);
+ if (original == null) return;
+ final ImageButton copy = new ImageButton(getContext(), null, R.attr.suggestionWordStyle);
+ copy.setTag(pinnedKey);
+ copy.setScaleType(original.getScaleType());
+ copy.setScaleX(original.getScaleX());
+ copy.setScaleY(original.getScaleY());
+ copy.setContentDescription(original.getContentDescription());
+ copy.setImageDrawable(original.getDrawable());
+ copy.setLayoutParams(original.getLayoutParams());
+ copy.setActivated(original.isActivated());
+ setupKey(copy, Settings.getValues().mColors);
+ mPinnedKeys.addView(copy);
+ }
+
+ private void setupKey(final ImageButton view, final Colors colors) {
+ view.setOnClickListener(this);
+ view.setOnLongClickListener(this);
+ colors.setColor(view, ColorType.TOOL_BAR_KEY);
+ colors.setBackground(view, ColorType.STRIP_BACKGROUND);
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt
deleted file mode 100644
index ae55648c8..000000000
--- a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt
+++ /dev/null
@@ -1,532 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- * modified
- * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
- */
-package helium314.keyboard.latin.suggestions
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.SharedPreferences
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener
-import android.graphics.Color
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.GradientDrawable
-import android.text.TextUtils
-import android.util.AttributeSet
-import android.util.TypedValue
-import android.view.GestureDetector
-import android.view.GestureDetector.SimpleOnGestureListener
-import android.view.LayoutInflater
-import android.view.MotionEvent
-import android.view.View
-import android.view.View.OnLongClickListener
-import android.view.ViewGroup
-import android.view.accessibility.AccessibilityEvent
-import android.widget.ImageButton
-import android.widget.LinearLayout
-import android.widget.RelativeLayout
-import android.widget.TextView
-import androidx.core.view.isVisible
-import helium314.keyboard.compat.isDeviceLocked
-import helium314.keyboard.keyboard.KeyboardSwitcher
-import helium314.keyboard.keyboard.internal.KeyboardIconsSet
-import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
-import helium314.keyboard.latin.AudioAndHapticFeedbackManager
-import helium314.keyboard.latin.Dictionary
-import helium314.keyboard.latin.R
-import helium314.keyboard.latin.SuggestedWords
-import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
-import helium314.keyboard.latin.common.ColorType
-import helium314.keyboard.latin.common.Colors
-import helium314.keyboard.latin.common.Constants
-import helium314.keyboard.latin.define.DebugFlags
-import helium314.keyboard.latin.settings.DebugSettings
-import helium314.keyboard.latin.settings.Defaults
-import helium314.keyboard.latin.settings.Settings
-import helium314.keyboard.latin.utils.Log
-import helium314.keyboard.latin.utils.ToolbarKey
-import helium314.keyboard.latin.utils.ToolbarMode
-import helium314.keyboard.latin.utils.addPinnedKey
-import helium314.keyboard.latin.utils.createToolbarKey
-import helium314.keyboard.latin.utils.getCodeForToolbarKey
-import helium314.keyboard.latin.utils.getCodeForToolbarKeyLongClick
-import helium314.keyboard.latin.utils.getEnabledToolbarKeys
-import helium314.keyboard.latin.utils.getPinnedToolbarKeys
-import helium314.keyboard.latin.utils.prefs
-import helium314.keyboard.latin.utils.removeFirst
-import helium314.keyboard.latin.utils.removePinnedKey
-import helium314.keyboard.latin.utils.setToolbarButtonsActivatedStateOnPrefChange
-import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.math.min
-
-@SuppressLint("InflateParams")
-class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int) :
- RelativeLayout(context, attrs, defStyle), View.OnClickListener, OnLongClickListener, OnSharedPreferenceChangeListener {
-
- /** Construct a [SuggestionStripView] for showing suggestions to be picked by the user. */
- constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.attr.suggestionStripViewStyle)
-
- interface Listener {
- fun pickSuggestionManually(word: SuggestedWordInfo?)
- fun onCodeInput(primaryCode: Int, x: Int, y: Int, isKeyRepeat: Boolean)
- fun removeSuggestion(word: String?)
- }
-
- private val moreSuggestionsContainer: View
- private val wordViews = ArrayList()
- private val debugInfoViews = ArrayList()
- private val dividerViews = ArrayList()
-
- init {
- val inflater = LayoutInflater.from(context)
- inflater.inflate(R.layout.suggestions_strip, this)
- moreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null)
-
- val colors = Settings.getValues().mColors
- colors.setBackground(this, ColorType.STRIP_BACKGROUND)
- val customTypeface = Settings.getInstance().customTypeface
- for (pos in 0..(R.id.suggestions_strip_toolbar_key)
- private val incognitoIcon = KeyboardIconsSet.instance.getNewDrawable(ToolbarKey.INCOGNITO.name, context)
- private val toolbarArrowIcon = KeyboardIconsSet.instance.getNewDrawable(KeyboardIconsSet.NAME_TOOLBAR_KEY, context)
- private val defaultToolbarBackground: Drawable = toolbarExpandKey.background
- private val enabledToolKeyBackground = GradientDrawable()
- private var direction = 1 // 1 if LTR, -1 if RTL
- init {
- val colors = Settings.getValues().mColors
-
- // expand key
- // weird way of setting size (default is config_suggestions_strip_edge_key_width)
- // but better not change it or people will complain
- val toolbarHeight = min(toolbarExpandKey.layoutParams.height, resources.getDimension(R.dimen.config_suggestions_strip_height).toInt())
- toolbarExpandKey.layoutParams.height = toolbarHeight
- toolbarExpandKey.layoutParams.width = toolbarHeight // we want it square
- colors.setBackground(toolbarExpandKey, ColorType.STRIP_BACKGROUND) // necessary because background is re-used for defaultToolbarBackground
- colors.setColor(toolbarExpandKey, ColorType.TOOL_BAR_EXPAND_KEY)
- colors.setColor(toolbarExpandKey.background, ColorType.TOOL_BAR_EXPAND_KEY_BACKGROUND)
-
- // background indicator for pinned keys
- val color = colors.get(ColorType.TOOL_BAR_KEY_ENABLED_BACKGROUND) or -0x1000000 // ignore alpha (in Java this is more readable 0xFF000000)
- enabledToolKeyBackground.colors = intArrayOf(color, Color.TRANSPARENT)
- enabledToolKeyBackground.gradientType = GradientDrawable.RADIAL_GRADIENT
- enabledToolKeyBackground.gradientRadius = resources.getDimensionPixelSize(R.dimen.config_suggestions_strip_height) / 2.1f
-
- val mToolbarMode = Settings.getValues().mToolbarMode
- if (mToolbarMode == ToolbarMode.TOOLBAR_KEYS) {
- setToolbarVisibility(true)
- }
-
- // toolbar keys setup
- val toolbarKeyLayoutParams = LinearLayout.LayoutParams(
- resources.getDimensionPixelSize(R.dimen.config_suggestions_strip_edge_key_width),
- LinearLayout.LayoutParams.MATCH_PARENT
- )
- if (mToolbarMode == ToolbarMode.TOOLBAR_KEYS || mToolbarMode == ToolbarMode.EXPANDABLE) {
- for (key in getEnabledToolbarKeys(context.prefs())) {
- val button = createToolbarKey(context, key)
- button.layoutParams = toolbarKeyLayoutParams
- setupKey(button, colors)
- toolbar.addView(button)
- }
- }
- if (!Settings.getValues().mSuggestionStripHiddenPerUserSettings) {
- for (pinnedKey in getPinnedToolbarKeys(context.prefs())) {
- val button = createToolbarKey(context, pinnedKey)
- button.layoutParams = toolbarKeyLayoutParams
- setupKey(button, colors)
- pinnedKeys.addView(button)
- val pinnedKeyInToolbar = toolbar.findViewWithTag(pinnedKey)
- if (pinnedKeyInToolbar != null && Settings.getValues().mQuickPinToolbarKeys)
- pinnedKeyInToolbar.background = enabledToolKeyBackground
- }
- }
-
- updateKeys()
- }
-
- private lateinit var listener: Listener
- private var suggestedWords = SuggestedWords.getEmptyInstance()
- private var startIndexOfMoreSuggestions = 0
- private var isExternalSuggestionVisible = false // Required to disable the more suggestions if other suggestions are visible
- private val layoutHelper = SuggestionStripLayoutHelper(context, attrs, defStyle, wordViews, dividerViews, debugInfoViews)
- private val moreSuggestionsView = moreSuggestionsContainer.findViewById(R.id.more_suggestions_view).apply {
- val slidingListener = object : SimpleOnGestureListener() {
- override fun onScroll(down: MotionEvent?, me: MotionEvent, deltaX: Float, deltaY: Float): Boolean {
- if (down == null) return false
- val dy = me.y - down.y
- return if (toolbarContainer.visibility != VISIBLE && deltaY > 0 && dy < 0) showMoreSuggestions()
- else false
- }
- }
- gestureDetector = GestureDetector(context, slidingListener)
- }
-
- // public stuff
-
- val isShowingMoreSuggestionPanel get() = moreSuggestionsView.isShowingInParent
-
- /** A connection back to the input method. */
- fun setListener(newListener: Listener, inputView: View) {
- listener = newListener
- moreSuggestionsView.listener = newListener
- moreSuggestionsView.mainKeyboardView = inputView.findViewById(R.id.keyboard_view)
- }
-
- fun setRtl(isRtlLanguage: Boolean) {
- val newLayoutDirection: Int
- if (!Settings.getValues().mVarToolbarDirection)
- newLayoutDirection = LAYOUT_DIRECTION_LOCALE
- else {
- newLayoutDirection = if (isRtlLanguage) LAYOUT_DIRECTION_RTL else LAYOUT_DIRECTION_LTR
- direction = if (isRtlLanguage) -1 else 1
- toolbarExpandKey.scaleX = (if (toolbarContainer.visibility != VISIBLE) 1f else -1f) * direction
- }
- layoutDirection = newLayoutDirection
- suggestionsStrip.layoutDirection = newLayoutDirection
- }
-
- fun setToolbarVisibility(toolbarVisible: Boolean) {
- // avoid showing toolbar keys when locked
- val locked = isDeviceLocked(context)
- pinnedKeys.isVisible = !locked && !toolbarVisible
- suggestionsStrip.isVisible = locked || !toolbarVisible
- toolbarContainer.isVisible = !locked && toolbarVisible
-
- if (DEBUG_SUGGESTIONS) {
- for (view in debugInfoViews) {
- view.visibility = suggestionsStrip.visibility
- }
- }
-
- toolbarExpandKey.scaleX = (if (toolbarVisible && !locked) -1f else 1f) * direction
- }
-
- fun setSuggestions(suggestions: SuggestedWords, isRtlLanguage: Boolean) {
- clear()
- setRtl(isRtlLanguage)
- suggestedWords = suggestions
- startIndexOfMoreSuggestions = layoutHelper.layoutAndReturnStartIndexOfMoreSuggestions(
- context, suggestedWords, suggestionsStrip, this
- )
- isExternalSuggestionVisible = false
- updateKeys();
- }
-
- fun setExternalSuggestionView(view: View?) {
- clear()
- isExternalSuggestionVisible = true
- suggestionsStrip.addView(view)
- if (Settings.getValues().mAutoHideToolbar) setToolbarVisibility(false)
- }
-
- fun setMoreSuggestionsHeight(remainingHeight: Int) {
- layoutHelper.setMoreSuggestionsHeight(remainingHeight)
- }
-
- fun dismissMoreSuggestionsPanel() {
- moreSuggestionsView.dismissPopupKeysPanel()
- }
-
- // overrides: necessarily public, but not used from outside
-
- override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String?) {
- setToolbarButtonsActivatedStateOnPrefChange(pinnedKeys, key)
- setToolbarButtonsActivatedStateOnPrefChange(toolbar, key)
- }
-
- override fun onVisibilityChanged(view: View, visibility: Int) {
- super.onVisibilityChanged(view, visibility)
- // workaround for a bug with inline suggestions views that just keep showing up otherwise, https://github.com/Helium314/HeliBoard/pull/386
- if (view === this)
- suggestionsStrip.visibility = visibility
- }
-
- override fun onDetachedFromWindow() {
- super.onDetachedFromWindow()
- dismissMoreSuggestionsPanel()
- }
-
- override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
- // Called by the framework when the size is known. Show the important notice if applicable.
- // This may be overridden by showing suggestions later, if applicable.
- }
-
- override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean {
- // Don't populate accessibility event with suggested words and voice key.
- return true
- }
-
- override fun onInterceptTouchEvent(motionEvent: MotionEvent): Boolean {
- // Disable More Suggestions if external suggestions are visible
- if (isExternalSuggestionVisible) {
- return false
- }
- // Detecting sliding up finger to show MoreSuggestionsView.
- return moreSuggestionsView.shouldInterceptTouchEvent(motionEvent)
- }
-
- @SuppressLint("ClickableViewAccessibility")
- override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
- moreSuggestionsView.touchEvent(motionEvent)
- return true
- }
-
- override fun onClick(view: View) {
- AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(KeyCode.NOT_SPECIFIED, this)
- val tag = view.tag
- if (tag is ToolbarKey) {
- val code = getCodeForToolbarKey(tag)
- if (code != KeyCode.UNSPECIFIED) {
- Log.d(TAG, "click toolbar key $tag")
- listener.onCodeInput(code, Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, false)
- if (tag === ToolbarKey.INCOGNITO) updateKeys() // update expand key icon
- return
- }
- }
- if (view === toolbarExpandKey) {
- setToolbarVisibility(toolbarContainer.visibility != VISIBLE)
- }
-
- // tag for word views is set in SuggestionStripLayoutHelper (setupWordViewsTextAndColor, layoutPunctuationSuggestions)
- if (tag is Int) {
- if (tag >= suggestedWords.size()) {
- return
- }
- val wordInfo = suggestedWords.getInfo(tag)
- listener.pickSuggestionManually(wordInfo)
- }
- }
-
- override fun onLongClick(view: View): Boolean {
- AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(Constants.NOT_A_CODE, this)
- if (view.tag is ToolbarKey) {
- onLongClickToolbarKey(view)
- return true
- }
- return if (view is TextView && wordViews.contains(view)) {
- onLongClickSuggestion(view)
- } else {
- showMoreSuggestions()
- }
- }
-
- // actually private stuff
-
- private fun onLongClickToolbarKey(view: View) {
- val tag = view.tag as? ToolbarKey ?: return
- if (!Settings.getValues().mQuickPinToolbarKeys || view.parent === pinnedKeys) {
- val longClickCode = getCodeForToolbarKeyLongClick(tag)
- if (longClickCode != KeyCode.UNSPECIFIED) {
- listener.onCodeInput(longClickCode, Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, false)
- }
- } else if (view.parent === toolbar) {
- val pinnedKeyView = pinnedKeys.findViewWithTag(tag)
- if (pinnedKeyView == null) {
- addKeyToPinnedKeys(tag)
- toolbar.findViewWithTag(tag).background = enabledToolKeyBackground
- addPinnedKey(context.prefs(), tag)
- } else {
- removePinnedKey(context.prefs(), tag)
- toolbar.findViewWithTag(tag).background = defaultToolbarBackground.constantState?.newDrawable(resources)
- pinnedKeys.removeView(pinnedKeyView)
- }
- }
- }
-
- @SuppressLint("ClickableViewAccessibility") // no need for View#performClick, we only return false mostly anyway
- private fun onLongClickSuggestion(wordView: TextView): Boolean {
- var showIcon = true
- if (wordView.tag is Int) {
- val index = wordView.tag as Int
- if (index < suggestedWords.size() && suggestedWords.getInfo(index).mSourceDict == Dictionary.DICTIONARY_USER_TYPED)
- showIcon = false
- }
- if (showIcon) {
- val icon = KeyboardIconsSet.instance.getNewDrawable(KeyboardIconsSet.NAME_BIN, context)!!
- Settings.getValues().mColors.setColor(icon, ColorType.REMOVE_SUGGESTION_ICON)
- val w = icon.intrinsicWidth
- val h = icon.intrinsicHeight
- wordView.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
- wordView.ellipsize = TextUtils.TruncateAt.END
- val downOk = AtomicBoolean(false)
- wordView.setOnTouchListener { _, motionEvent ->
- if (motionEvent.action == MotionEvent.ACTION_UP && downOk.get()) {
- val x = motionEvent.x
- val y = motionEvent.y
- if (0 < x && x < w && 0 < y && y < h) {
- removeSuggestion(wordView)
- wordView.cancelLongPress()
- wordView.isPressed = false
- return@setOnTouchListener true
- }
- } else if (motionEvent.action == MotionEvent.ACTION_DOWN) {
- val x = motionEvent.x
- val y = motionEvent.y
- if (0 < x && x < w && 0 < y && y < h) {
- downOk.set(true)
- }
- }
- false
- }
- }
- if (DebugFlags.DEBUG_ENABLED && (isShowingMoreSuggestionPanel || !showMoreSuggestions())) {
- showSourceDict(wordView)
- return true
- }
- return showMoreSuggestions()
- }
-
- private fun showMoreSuggestions(): Boolean {
- if (suggestedWords.size() <= startIndexOfMoreSuggestions) {
- return false
- }
- if (!moreSuggestionsView.show(
- suggestedWords, startIndexOfMoreSuggestions, moreSuggestionsContainer, layoutHelper, this
- ))
- return false
- for (i in 0..= suggestedWords.size()) return
- val info = suggestedWords.getInfo(index)
- if (info.word != word) return
-
- val text = info.mSourceDict.mDictType + ":" + info.mSourceDict.mLocale
- if (isShowingMoreSuggestionPanel) {
- moreSuggestionsView.dismissPopupKeysPanel()
- }
- KeyboardSwitcher.getInstance().showToast(text, true)
- }
-
- private fun removeSuggestion(wordView: TextView) {
- val word = wordView.text.toString()
- listener.removeSuggestion(word)
- moreSuggestionsView.dismissPopupKeysPanel()
- // show suggestions, but without the removed word
- val suggestedWordInfos = ArrayList()
- for (i in 0..(ToolbarKey.VOICE)?.isVisible = show
- pinnedKeys.findViewWithTag(ToolbarKey.VOICE)?.isVisible = show
- }
-
- private fun updateKeys() {
- updateVoiceKey()
- val settingsValues = Settings.getValues()
-
- val toolbarIsExpandable = settingsValues.mToolbarMode == ToolbarMode.EXPANDABLE
- if (settingsValues.mIncognitoModeEnabled) {
- toolbarExpandKey.setImageDrawable(incognitoIcon)
- toolbarExpandKey.isVisible = true
- } else {
- toolbarExpandKey.setImageDrawable(toolbarArrowIcon)
- toolbarExpandKey.isVisible = toolbarIsExpandable
- }
-
- // hide pinned keys if device is locked, and avoid expanding toolbar
- val hideToolbarKeys = isDeviceLocked(context)
- toolbarExpandKey.setOnClickListener(if (hideToolbarKeys || !toolbarIsExpandable) null else this)
- pinnedKeys.visibility = if (hideToolbarKeys) GONE else suggestionsStrip.visibility
- isExternalSuggestionVisible = false
- }
-
- private fun addKeyToPinnedKeys(pinnedKey: ToolbarKey) {
- val original = toolbar.findViewWithTag(pinnedKey) ?: return
- // copy the original key to a new ImageButton
- val copy = ImageButton(context, null, R.attr.suggestionWordStyle)
- copy.tag = pinnedKey
- copy.scaleType = original.scaleType
- copy.scaleX = original.scaleX
- copy.scaleY = original.scaleY
- copy.contentDescription = original.contentDescription
- copy.setImageDrawable(original.drawable)
- copy.layoutParams = original.layoutParams
- copy.isActivated = original.isActivated
- setupKey(copy, Settings.getValues().mColors)
- pinnedKeys.addView(copy)
- }
-
- private fun setupKey(view: ImageButton, colors: Colors) {
- view.setOnClickListener(this)
- view.setOnLongClickListener(this)
- colors.setColor(view, ColorType.TOOL_BAR_KEY)
- colors.setBackground(view, ColorType.STRIP_BACKGROUND)
- }
-
- companion object {
- @JvmField
- var DEBUG_SUGGESTIONS = false
- private const val DEBUG_INFO_TEXT_SIZE_IN_DIP = 6.5f
- private val TAG = SuggestionStripView::class.java.simpleName
- }
-}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.java
new file mode 100644
index 000000000..b27de7655
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.latin.utils;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
+
+import helium314.keyboard.latin.define.DecoderSpecificConstants;
+import helium314.keyboard.latin.makedict.DictionaryHeader;
+import helium314.keyboard.latin.makedict.UnsupportedFormatException;
+import helium314.keyboard.latin.settings.SpacingAndPunctuations;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * This class encapsulates the logic for the Latin-IME side of dictionary information management.
+ */
+public class DictionaryInfoUtils {
+ private static final String TAG = DictionaryInfoUtils.class.getSimpleName();
+ public static final String DEFAULT_MAIN_DICT = "main";
+ public static final String USER_DICTIONARY_SUFFIX = "user.dict";
+ public static final String MAIN_DICT_PREFIX = DEFAULT_MAIN_DICT + "_";
+ // 6 digits - unicode is limited to 21 bits
+ private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6;
+ public static final String ASSETS_DICTIONARY_FOLDER = "dicts";
+ public static final String ID_CATEGORY_SEPARATOR = ":";
+ private static final String DICTIONARY_CATEGORY_SEPARATOR_EXPRESSION = "[" + ID_CATEGORY_SEPARATOR + "_]";
+
+ private DictionaryInfoUtils() {
+ // Private constructor to forbid instantation of this helper class.
+ }
+
+ /**
+ * Returns whether we may want to use this character as part of a file name.
+ *
+ * This basically only accepts ascii letters and numbers, and rejects everything else.
+ */
+ private static boolean isFileNameCharacter(int codePoint) {
+ if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit
+ if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase
+ if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase
+ return codePoint == '_' || codePoint == '-';
+ }
+
+ /**
+ * Escapes a string for any characters that may be suspicious for a file or directory name.
+ *
+ * Concretely this does a sort of URL-encoding except it will encode everything that's not
+ * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
+ * we cannot allow here)
+ */
+ // TODO: create a unit test for this method
+ public static String replaceFileNameDangerousCharacters(final String name) {
+ // This assumes '%' is fully available as a non-separator, normal
+ // character in a file name. This is probably true for all file systems.
+ final StringBuilder sb = new StringBuilder();
+ final int nameLength = name.length();
+ for (int i = 0; i < nameLength; i = name.offsetByCodePoints(i, 1)) {
+ final int codePoint = name.codePointAt(i);
+ if (DictionaryInfoUtils.isFileNameCharacter(codePoint)) {
+ sb.appendCodePoint(codePoint);
+ } else {
+ sb.append(String.format(Locale.US, "%%%1$0" + MAX_HEX_DIGITS_FOR_CODEPOINT + "x", codePoint));
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Helper method to get the top level cache directory.
+ */
+ public static String getWordListCacheDirectory(final Context context) {
+ return context.getFilesDir() + File.separator + "dicts";
+ }
+
+ /**
+ * Reverse escaping done by {@link #replaceFileNameDangerousCharacters(String)}.
+ */
+ @NonNull
+ public static String getWordListIdFromFileName(@NonNull final String fname) {
+ final StringBuilder sb = new StringBuilder();
+ final int fnameLength = fname.length();
+ for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) {
+ final int codePoint = fname.codePointAt(i);
+ if ('%' != codePoint) {
+ sb.appendCodePoint(codePoint);
+ } else {
+ // + 1 to pass the % sign
+ final int encodedCodePoint =
+ Integer.parseInt(fname.substring(i + 1, i + 1 + MAX_HEX_DIGITS_FOR_CODEPOINT), 16);
+ i += MAX_HEX_DIGITS_FOR_CODEPOINT;
+ sb.appendCodePoint(encodedCodePoint);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Helper method to the list of cache directories, one for each distinct locale.
+ */
+ @Nullable public static File[] getCachedDirectoryList(final Context context) {
+ return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles();
+ }
+
+ /**
+ * Find out the cache directory associated with a specific locale.
+ */
+ public static String getAndCreateCacheDirectoryForLocale(final Locale locale, final Context context) {
+ final String absoluteDirectoryName = getCacheDirectoryForLocale(locale, context);
+ final File directory = new File(absoluteDirectoryName);
+ if (!directory.exists()) {
+ if (!directory.mkdirs()) {
+ Log.e(TAG, "Could not create the directory for locale" + locale);
+ }
+ }
+ return absoluteDirectoryName;
+ }
+
+ public static String getCacheDirectoryForLocale(final Locale locale, final Context context) {
+ final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale.toLanguageTag());
+ return getWordListCacheDirectory(context) + File.separator + relativeDirectoryName;
+ }
+
+ public static File[] getCachedDictsForLocale(final Locale locale, final Context context) {
+ final File cachedDir = new File(getAndCreateCacheDirectoryForLocale(locale, context));
+ if (!cachedDir.isDirectory())
+ return new File[]{};
+ return cachedDir.listFiles();
+ }
+
+ public static String getExtractedMainDictFilename() {
+ return DEFAULT_MAIN_DICT + ".dict";
+ }
+
+ @Nullable
+ public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file,
+ final long offset, final long length) {
+ try {
+ return BinaryDictionaryUtils.getHeaderWithOffsetAndLength(file, offset, length);
+ } catch (UnsupportedFormatException | IOException e) {
+ return null;
+ }
+ }
+
+ @Nullable
+ public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file) {
+ try {
+ return BinaryDictionaryUtils.getHeader(file);
+ } catch (UnsupportedFormatException | IOException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the locale for a dictionary file name stored in assets.
+ *
+ * Assumes file name main_[locale].dict
+ *
+ * Returns the locale, or null if file name does not match the pattern
+ */
+ @Nullable public static String extractLocaleFromAssetsDictionaryFile(final String dictionaryFileName) {
+ if (dictionaryFileName.startsWith(DictionaryInfoUtils.MAIN_DICT_PREFIX)
+ && dictionaryFileName.endsWith(".dict")) {
+ return dictionaryFileName.substring(
+ DictionaryInfoUtils.MAIN_DICT_PREFIX.length(),
+ dictionaryFileName.lastIndexOf('.')
+ );
+ }
+ return null;
+ }
+
+ @Nullable public static String[] getAssetsDictionaryList(final Context context) {
+ final String[] dictionaryList;
+ try {
+ dictionaryList = context.getAssets().list(ASSETS_DICTIONARY_FOLDER);
+ } catch (IOException e) {
+ return null;
+ }
+ return dictionaryList;
+ }
+
+ public static boolean looksValidForDictionaryInsertion(final CharSequence text,
+ final SpacingAndPunctuations spacingAndPunctuations) {
+ if (TextUtils.isEmpty(text)) {
+ return false;
+ }
+ final int length = text.length();
+ if (length > DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH) {
+ return false;
+ }
+ int i = 0;
+ int digitCount = 0;
+ while (i < length) {
+ final int codePoint = Character.codePointAt(text, i);
+ final int charCount = Character.charCount(codePoint);
+ i += charCount;
+ if (Character.isDigit(codePoint)) {
+ // Count digits: see below
+ digitCount += charCount;
+ continue;
+ }
+ if (!spacingAndPunctuations.isWordCodePoint(codePoint)) {
+ return false;
+ }
+ }
+ // We reject strings entirely comprised of digits to avoid using PIN codes or credit
+ // card numbers. It would come in handy for word prediction though; a good example is
+ // when writing one's address where the street number is usually quite discriminative,
+ // as well as the postal code.
+ return digitCount < length;
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.kt
deleted file mode 100644
index 05417fdb3..000000000
--- a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- * modified
- * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
- */
-package helium314.keyboard.latin.utils
-
-import android.content.Context
-import android.text.TextUtils
-import com.android.inputmethod.latin.utils.BinaryDictionaryUtils
-import helium314.keyboard.latin.common.FileUtils
-import helium314.keyboard.latin.common.LocaleUtils.constructLocale
-import helium314.keyboard.latin.common.loopOverCodePoints
-import helium314.keyboard.latin.define.DecoderSpecificConstants
-import helium314.keyboard.latin.makedict.DictionaryHeader
-import helium314.keyboard.latin.makedict.UnsupportedFormatException
-import helium314.keyboard.latin.settings.SpacingAndPunctuations
-import java.io.File
-import java.io.IOException
-import java.util.Locale
-
-/** encapsulates the logic for the Latin-IME side of dictionary information management */
-object DictionaryInfoUtils {
- private val TAG = DictionaryInfoUtils::class.java.simpleName
- const val DEFAULT_MAIN_DICT = "main"
- const val USER_DICTIONARY_SUFFIX = "user.dict"
- const val MAIN_DICT_PREFIX = DEFAULT_MAIN_DICT + "_"
- const val ASSETS_DICTIONARY_FOLDER = "dicts"
- const val MAIN_DICT_FILE_NAME = DEFAULT_MAIN_DICT + ".dict"
- private const val MAX_HEX_DIGITS_FOR_CODEPOINT = 6 // unicode is limited to 21 bits
-
- /**
- * Returns whether we may want to use this character as part of a file name.
- * This basically only accepts ascii letters and numbers, and rejects everything else.
- */
- private fun isFileNameCharacter(codePoint: Int): Boolean {
- if (codePoint in 0x30..0x39) return true // Digit
- if (codePoint in 0x41..0x5A) return true // Uppercase
- if (codePoint in 0x61..0x7A) return true // Lowercase
- return codePoint == '_'.code || codePoint == '-'.code
- }
-
- /**
- * Escapes a string for any characters that may be suspicious for a file or directory name.
- *
- * Concretely this does a sort of URL-encoding except it will encode everything that's not
- * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
- * we cannot allow here)
- */
- private fun replaceFileNameDangerousCharacters(name: String): String {
- // This assumes '%' is fully available as a non-separator, normal
- // character in a file name. This is probably true for all file systems.
- val sb = StringBuilder()
- loopOverCodePoints(name) { codePoint, _ ->
- if (isFileNameCharacter(codePoint)) {
- sb.appendCodePoint(codePoint)
- } else {
- sb.append(String.format(Locale.US, "%%%1$0" + MAX_HEX_DIGITS_FOR_CODEPOINT + "x", codePoint))
- }
- false
- }
- return sb.toString()
- }
-
- // we cache the extracted dictionaries in filesDir, because actual cache might be cleared at
- // any time, and we can't permanently check whether the dictionary still exists
- fun getWordListCacheDirectory(context: Context): String = context.filesDir.toString() + File.separator + "dicts"
-
- /** Reverse escaping done by replaceFileNameDangerousCharacters. */
- fun getWordListIdFromFileName(fname: String): String {
- val sb = StringBuilder()
- val fnameLength = fname.length
- var i = 0
- while (i < fnameLength) {
- val codePoint = fname.codePointAt(i)
- if ('%'.code != codePoint) {
- sb.appendCodePoint(codePoint)
- } else {
- // + 1 to pass the % sign
- val encodedCodePoint = fname.substring(i + 1, i + 1 + MAX_HEX_DIGITS_FOR_CODEPOINT).toInt(16)
- i += MAX_HEX_DIGITS_FOR_CODEPOINT
- sb.appendCodePoint(encodedCodePoint)
- }
- i = fname.offsetByCodePoints(i, 1)
- }
- return sb.toString()
- }
-
- /** Helper method to the list of cache directories, one for each distinct locale. */
- fun getCacheDirectories(context: Context) = File(getWordListCacheDirectory(context)).listFiles().orEmpty()
-
- /** Find out the cache directory associated with a specific locale. */
- fun getCacheDirectoryForLocale(locale: Locale, context: Context): String? {
- val relativeDirectoryName = replaceFileNameDangerousCharacters(locale.toLanguageTag())
- val absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator + relativeDirectoryName
- val directory = File(absoluteDirectoryName)
- if (!directory.exists() && !directory.mkdirs()) {
- Log.e(TAG, "Could not create the directory for locale $locale")
- return null
- }
- return absoluteDirectoryName
- }
-
- @JvmStatic
- fun getCachedDictForLocaleAndType(locale: Locale, type: String, context: Context): File? =
- getCachedDictsForLocale(locale, context).firstOrNull { it.name.substringBefore("_") == type }
-
- fun getCachedDictsForLocale(locale: Locale, context: Context) =
- getCacheDirectoryForLocale(locale, context)?.let { File(it).listFiles() }.orEmpty()
-
- fun getDictionaryFileHeaderOrNull(file: File, offset: Long, length: Long): DictionaryHeader? {
- return try {
- BinaryDictionaryUtils.getHeaderWithOffsetAndLength(file, offset, length)
- } catch (e: UnsupportedFormatException) {
- null
- } catch (e: IOException) {
- null
- }
- }
-
- fun getDictionaryFileHeaderOrNull(file: File): DictionaryHeader? {
- return try {
- BinaryDictionaryUtils.getHeader(file)
- } catch (e: UnsupportedFormatException) {
- null
- } catch (e: IOException) {
- null
- }
- }
-
- /**
- * Returns the locale for a dictionary file name stored in assets.
- *
- * Assumes file name main_[locale].dict
- * Returns the locale, or null if file name does not match the pattern
- */
- fun extractLocaleFromAssetsDictionaryFile(dictionaryFileName: String): Locale {
- if (dictionaryFileName.contains('_') && !dictionaryFileName.contains('.'))
- throw IllegalStateException("invalid asset dictionary name $dictionaryFileName")
- return dictionaryFileName.substringAfter("_").substringBefore(".").constructLocale()
- }
-
- // actually we could extract assets dicts to unprotected storage
- fun extractAssetsDictionary(dictionaryFileName: String, locale: Locale, context: Context): File? {
- val cacheDir = getCacheDirectoryForLocale(locale, context) ?: return null
- val targetFile = File(cacheDir, "${dictionaryFileName.substringBefore("_")}.dict")
- try {
- FileUtils.copyStreamToNewFile(
- context.assets.open(ASSETS_DICTIONARY_FOLDER + File.separator + dictionaryFileName),
- targetFile
- )
- } catch (e: IOException) {
- Log.e(TAG, "Could not extract assets dictionary $dictionaryFileName")
- return null
- }
- return targetFile
- }
-
- fun getAssetsDictionaryList(context: Context): Array? = try {
- context.assets.list(ASSETS_DICTIONARY_FOLDER)
- } catch (e: IOException) {
- null
- }
-
- @JvmStatic
- fun looksValidForDictionaryInsertion(text: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
- if (TextUtils.isEmpty(text)) {
- return false
- }
- if (text.length > DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH) {
- return false
- }
- var digitCount = 0
- loopOverCodePoints(text) { codePoint, charCount ->
- if (Character.isDigit(codePoint)) {
- // Count digits: see below
- digitCount += charCount
- return@loopOverCodePoints false
- }
- if (!spacingAndPunctuations.isWordCodePoint(codePoint)) {
- return false
- }
- false
- }
- // We reject strings entirely comprised of digits to avoid using PIN codes or credit
- // card numbers. It would come in handy for word prediction though; a good example is
- // when writing one's address where the street number is usually quite discriminative,
- // as well as the postal code.
- return digitCount < text.length
- }
-}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt
index aa794f27c..3bbb310c4 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt
@@ -25,7 +25,7 @@ fun getDictionaryLocales(context: Context): MutableSet {
val locales = HashSet()
// get cached dictionaries: extracted or user-added dictionaries
- DictionaryInfoUtils.getCacheDirectories(context).forEach { directory ->
+ DictionaryInfoUtils.getCachedDirectoryList(context)?.forEach { directory ->
if (!directory.isDirectory) return@forEach
if (!hasAnythingOtherThanExtractedMainDictionary(directory)) return@forEach
val locale = DictionaryInfoUtils.getWordListIdFromFileName(directory.name).constructLocale()
@@ -35,7 +35,8 @@ fun getDictionaryLocales(context: Context): MutableSet {
val assetsDictionaryList = DictionaryInfoUtils.getAssetsDictionaryList(context)
if (assetsDictionaryList != null) {
for (dictionary in assetsDictionaryList) {
- locales.add(DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dictionary))
+ val locale = DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dictionary)?.constructLocale() ?: continue
+ locales.add(locale)
}
}
return locales
@@ -86,6 +87,7 @@ fun MissingDictionaryDialog(onDismissRequest: () -> Unit, locale: Locale) {
fun createDictionaryTextAnnotated(locale: Locale): AnnotatedString {
val knownDicts = mutableListOf>()
val builder = AnnotatedString.Builder()
+ builder.appendLine(stringResource(R.string.dictionary_available))
val context = LocalContext.current
context.assets.open("dictionaries_in_dict_repo.csv").reader().forEachLine {
if (it.isBlank()) return@forEachLine
@@ -133,4 +135,4 @@ fun cleanUnusedMainDicts(context: Context) {
}
private fun hasAnythingOtherThanExtractedMainDictionary(dir: File) =
- dir.listFiles()?.any { it.name != DictionaryInfoUtils.MAIN_DICT_FILE_NAME } != false
+ dir.listFiles()?.any { it.name != DictionaryInfoUtils.getExtractedMainDictFilename() } != false
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/InputMethodPicker.kt b/app/src/main/java/helium314/keyboard/latin/utils/InputMethodPicker.kt
index 71c5bda4d..3f534842f 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/InputMethodPicker.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/InputMethodPicker.kt
@@ -14,7 +14,6 @@ import android.view.inputmethod.InputMethodSubtype
import helium314.keyboard.latin.LatinIME
import helium314.keyboard.latin.R
import helium314.keyboard.latin.RichInputMethodManager
-import helium314.keyboard.latin.utils.SubtypeLocaleUtils.displayName
// similar to what showSubtypePicker does in https://github.com/rkkr/simple-keyboard/blob/master/app/src/main/java/rkr/simplekeyboard/inputmethod/latin/RichInputMethodManager.java
fun createInputMethodPickerDialog(latinIme: LatinIME, richImm: RichInputMethodManager, windowToken: IBinder): AlertDialog {
@@ -26,8 +25,8 @@ fun createInputMethodPickerDialog(latinIme: LatinIME, richImm: RichInputMethodMa
val enabledSubtypes = mutableListOf>()
var currentSubtypeIndex = 0
enabledImis.forEach { imi ->
- val subtypes = if (imi != thisImi) richImm.getEnabledInputMethodSubtypes(imi, true)
- else richImm.getEnabledInputMethodSubtypes(imi, true).sortedBy { it.displayName() }
+ val subtypes = if (imi != thisImi) richImm.getEnabledInputMethodSubtypeList(imi, true)
+ else richImm.getEnabledInputMethodSubtypeList(imi, true).sortedBy { it.displayName(latinIme).toString() }
if (subtypes.isEmpty()) {
enabledSubtypes.add(imi to null)
} else {
@@ -45,7 +44,7 @@ fun createInputMethodPickerDialog(latinIme: LatinIME, richImm: RichInputMethodMa
for (imiAndSubtype in enabledSubtypes) {
val (imi, subtype) = imiAndSubtype
- val subtypeName = if (imi == thisImi) subtype?.displayName()
+ val subtypeName = if (imi == thisImi) subtype?.displayName(latinIme)
else subtype?.getDisplayName(latinIme, imi.packageName, imi.serviceInfo.applicationInfo)
val title = SpannableString(subtypeName?.ifBlank { imi.loadLabel(pm) } ?: imi.loadLabel(pm))
val subtitle = SpannableString(if (subtype == null) "" else "\n${imi.loadLabel(pm)}")
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/IntentUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/IntentUtils.kt
deleted file mode 100644
index da1b06bd8..000000000
--- a/app/src/main/java/helium314/keyboard/latin/utils/IntentUtils.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package helium314.keyboard.latin.utils
-
-import android.content.Context
-import android.content.Intent
-import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
-import helium314.keyboard.latin.inputlogic.InputLogic
-import helium314.keyboard.latin.utils.Log.i
-
-object IntentUtils {
- val TAG: String = InputLogic::class.java.simpleName
- private val ACTION_SEND_INTENT = "helium314.keyboard.latin.ACTION_SEND_INTENT"
- private val EXTRA_NUMBER = "EXTRA_NUMBER"
-
- @JvmStatic
- fun handleSendIntentKey(context: Context, mKeyCode: Int) {
- val intentNumber = (KeyCode.SEND_INTENT_ONE + 1) - mKeyCode;
-
- val intent: Intent = Intent(ACTION_SEND_INTENT).apply {
- putExtra(EXTRA_NUMBER, intentNumber)
- }
-
- context.sendBroadcast(intent)
- i(TAG, "Sent broadcast for intent number: $intentNumber")
- }
-}
-
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java
index 310ece153..6bae92d5e 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java
+++ b/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java
@@ -52,18 +52,10 @@ public final class JniUtils {
.getMethod("currentApplication").invoke(null, (Object[]) null);
} catch (Exception ignored) { }
}
- if (app != null && app.getFilesDir() != null) // use the actual path if possible
+ if (app != null) // use the actual path if possible
filesDir = app.getFilesDir().getAbsolutePath();
-
- File userSuppliedLibrary;
- try {
- userSuppliedLibrary = new File(filesDir + File.separator + JNI_LIB_IMPORT_FILE_NAME);
- if (!userSuppliedLibrary.isFile())
- userSuppliedLibrary = null;
- } catch (Exception e) {
- userSuppliedLibrary = null;
- }
- if (!BuildConfig.BUILD_TYPE.equals("nouserlib") && userSuppliedLibrary != null) {
+ final File userSuppliedLibrary = new File(filesDir + File.separator + JNI_LIB_IMPORT_FILE_NAME);
+ if (!BuildConfig.BUILD_TYPE.equals("nouserlib") && userSuppliedLibrary.exists()) {
String wantedChecksum = expectedDefaultChecksum();
try {
if (app != null) {
@@ -98,7 +90,7 @@ public final class JniUtils {
System.loadLibrary(JNI_LIB_NAME_GOOGLE);
sHaveGestureLib = true;
} catch (UnsatisfiedLinkError ul) {
- Log.w(TAG, "Could not load system glide typing library " + JNI_LIB_NAME_GOOGLE + ": " + ul.getMessage());
+ Log.w(TAG, "Could not load system glide typing library " + JNI_LIB_NAME_GOOGLE, ul);
}
}
if (!sHaveGestureLib) {
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/LanguageOnSpacebarUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/LanguageOnSpacebarUtils.java
index f7c400da2..f942157ca 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/LanguageOnSpacebarUtils.java
+++ b/app/src/main/java/helium314/keyboard/latin/utils/LanguageOnSpacebarUtils.java
@@ -43,12 +43,16 @@ public final class LanguageOnSpacebarUtils {
return FORMAT_TYPE_NONE;
}
final Locale locale = subtype.getLocale();
+ if (locale == null) {
+ return FORMAT_TYPE_NONE;
+ }
final String keyboardLanguage = locale.getLanguage();
final String keyboardLayout = subtype.getMainLayoutName();
int sameLanguageAndLayoutCount = 0;
for (final InputMethodSubtype ims : sEnabledSubtypes) {
final String language = SubtypeUtilsKt.locale(ims).getLanguage();
- if (keyboardLanguage.equals(language) && keyboardLayout.equals(SubtypeUtilsKt.mainLayoutNameOrQwerty(ims))) {
+ if (keyboardLanguage.equals(language) && keyboardLayout.equals(
+ SubtypeLocaleUtils.getMainLayoutName(ims))) {
sameLanguageAndLayoutCount++;
}
}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt b/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt
index 00db72c87..412bb158c 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt
@@ -13,9 +13,9 @@ enum class LayoutType {
companion object {
fun EnumMap.toExtraValue() = map { it.key.name + Separators.KV + it.value }.joinToString(Separators.ENTRY)
- fun getLayoutMap(string: String?): EnumMap {
+ fun getLayoutMap(string: String): EnumMap {
val map = EnumMap(LayoutType::class.java)
- string?.split(Separators.ENTRY)?.forEach {
+ string.split(Separators.ENTRY).forEach {
val s = it.split(Separators.KV)
runCatching { map[LayoutType.valueOf(s[0])] = s[1] }
}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt
index 13bcb24c0..68a99be99 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt
@@ -17,7 +17,7 @@ object LayoutUtils {
return context.assets.list(layoutType.folder)?.map { it.substringBefore(".") }.orEmpty()
if (locale == null)
return SubtypeSettings.getAllAvailableSubtypes()
- .mapTo(HashSet()) { it.mainLayoutNameOrQwerty().substringBefore("+") }
+ .mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" }
.apply { addAll(context.resources.getStringArray(R.array.predefined_layouts)) }
val layouts = SubtypeSettings.getResourceSubtypesForLocale(locale).mapNotNullTo(mutableSetOf()) { it.mainLayoutName() }
if (locale.script() == ScriptUtils.SCRIPT_LATIN)
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtilsCustom.kt b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtilsCustom.kt
index 4f8686442..bc40baed5 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtilsCustom.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtilsCustom.kt
@@ -2,7 +2,6 @@
package helium314.keyboard.latin.utils
import android.content.Context
-import android.widget.Toast
import helium314.keyboard.keyboard.Key
import helium314.keyboard.keyboard.KeyboardId
import helium314.keyboard.keyboard.KeyboardLayoutSet
@@ -11,14 +10,9 @@ import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.LayoutParser
import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL
import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
-import helium314.keyboard.latin.common.Constants.Separators
-import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET
import helium314.keyboard.latin.common.decodeBase36
import helium314.keyboard.latin.common.encodeBase36
-import helium314.keyboard.latin.define.DebugFlags
-import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
-import helium314.keyboard.latin.settings.SettingsSubtype.Companion.toSettingsSubtype
import helium314.keyboard.latin.utils.LayoutType.Companion.folder
import helium314.keyboard.latin.utils.ScriptUtils.script
import kotlinx.serialization.SerializationException
@@ -36,13 +30,11 @@ object LayoutUtilsCustom {
params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT)
addLocaleKeyTextsToParams(context, params, POPUP_KEYS_NORMAL)
try {
- if (layoutContent.trimStart().startsWith("[") || layoutContent.trimStart().startsWith("//")) {
- val keys = LayoutParser.parseJsonString(layoutContent).map { row -> row.mapNotNull { it.compute(params)?.toKeyParams(params) } }
- return checkKeys(keys)
- }
+ val keys = LayoutParser.parseJsonString(layoutContent).map { row -> row.mapNotNull { it.compute(params)?.toKeyParams(params) } }
+ return checkKeys(keys)
} catch (e: SerializationException) {
Log.w(TAG, "json parsing error", e)
- if (layoutContent.trimEnd().endsWith("]") && layoutContent.contains("},"))
+ if (layoutContent.trimStart().startsWith("[") && layoutContent.trimEnd().endsWith("]") && layoutContent.contains("},"))
return false // we're sure enough it's a json
} catch (e: Exception) {
Log.w(TAG, "json layout parsed, but considered invalid", e)
@@ -132,6 +124,7 @@ object LayoutUtilsCustom {
layoutName
}
+
/** @return layoutName for given [displayName]. If [layoutType ]is MAIN, non-null [locale] must be supplied */
fun getLayoutName(displayName: String, layoutType: LayoutType, locale: Locale? = null): String {
if (layoutType != LayoutType.MAIN)
@@ -150,37 +143,6 @@ object LayoutUtilsCustom {
return file
}
- // remove layouts without a layout file from custom subtypes and settings
- // should not be necessary, but better fall back to default instead of crashing when encountering a bug
- fun removeMissingLayouts(context: Context) {
- val prefs = context.prefs()
- fun remove(type: LayoutType, name: String) {
- val message = "removing custom layout ${getDisplayName(name)} / $name without file"
- if (DebugFlags.DEBUG_ENABLED)
- Toast.makeText(context, message, Toast.LENGTH_LONG).show()
- Log.w(TAG, message)
- SubtypeSettings.onRenameLayout(type, name, null, context)
- }
- LayoutType.entries.forEach { type ->
- val name = Settings.readDefaultLayoutName(type, prefs)
- if (!isCustomLayout(name) || getLayoutFiles(type, context).any { it.name.startsWith(name) })
- return@forEach
- remove(type, name)
- }
- prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
- .split(Separators.SETS).forEach outer@{
- val subtype = it.toSettingsSubtype()
- LayoutType.getLayoutMap(subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET) ?: "").forEach { (type, name) ->
- if (!isCustomLayout(name) || getLayoutFiles(type, context).any { it.name.startsWith(name) })
- return@forEach
- remove(type, name)
- // recursive call: additional subtypes must have changed, so we repeat until nothing needs to be deleted
- removeMissingLayouts(context)
- return
- }
- }
- }
-
// this goes into prefs and file names, so do not change!
const val CUSTOM_LAYOUT_PREFIX = "custom."
private const val TAG = "LayoutUtilsCustom"
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/PopupKeysUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/PopupKeysUtils.kt
index 6bd810d05..5d47d27b8 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/PopupKeysUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/PopupKeysUtils.kt
@@ -72,10 +72,10 @@ fun getHintLabel(popupSet: PopupSet<*>?, params: KeyboardParams, label: String):
}
if (hintLabel != null) break
}
- if (hintLabel in toolbarKeyStrings.values || hintLabel.isNullOrEmpty())
- return null // better show nothing instead of the toolbar key label
+ if (hintLabel in toolbarKeyStrings.values)
+ hintLabel = null // better show nothing instead of the toolbar key label
- return KeySpecParser.getLabel(transformLabel(hintLabel!!, params))
+ return hintLabel?.let { KeySpecParser.getLabel(transformLabel(it, params)) }
// avoid e.g. !autoColumnOrder! as label
// this will avoid having labels on comma and period keys
?.takeIf { !it.startsWith("!") || it.count { it == '!' } != 2 } // excluding the special labels
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/ResourceUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/ResourceUtils.java
index be44521d3..3ff53dadd 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/ResourceUtils.java
+++ b/app/src/main/java/helium314/keyboard/latin/utils/ResourceUtils.java
@@ -55,15 +55,6 @@ public final class ResourceUtils {
return windowBounds.width() - insets.left - insets.right;
}
- public static int getSecondaryKeyboardHeight(final Resources res, final SettingsValues settingsValues) {
- final int keyboardHeight = getKeyboardHeight(res, settingsValues);
- if (settingsValues.mToolbarMode == ToolbarMode.HIDDEN && ! settingsValues.mToolbarHidingGlobal) {
- // Small adjustment to match the height of the main keyboard which has a hidden strip container.
- return keyboardHeight - (int) res.getDimension(R.dimen.config_suggestions_strip_height);
- }
- return keyboardHeight;
- }
-
public static int getKeyboardHeight(final Resources res, final SettingsValues settingsValues) {
final int defaultKeyboardHeight = getDefaultKeyboardHeight(res, settingsValues.mShowsNumberRow);
// mKeyboardHeightScale Ranges from [.5,1.5], from xml/prefs_screen_appearance.xml
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/ScriptUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/ScriptUtils.kt
index ba0fca550..f66c76aba 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/ScriptUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/ScriptUtils.kt
@@ -168,7 +168,7 @@ object ScriptUtils {
"sr", "mk", "ru", "uk", "mn", "be", "kk", "ky", "bg", "xdq", "cv", "mhr", "mns", "dru" -> SCRIPT_CYRILLIC
"ka" -> SCRIPT_GEORGIAN
"el" -> SCRIPT_GREEK
- "iw", "he" -> SCRIPT_HEBREW
+ "iw" -> SCRIPT_HEBREW
"km" -> SCRIPT_KHMER
"lo" -> SCRIPT_LAO
"ml" -> SCRIPT_MALAYALAM
@@ -184,12 +184,4 @@ object ScriptUtils {
else -> SCRIPT_LATIN // use as fallback
}
}
-
- @JvmStatic
- fun isScriptRtl(script: String): Boolean {
- return when (script) {
- SCRIPT_ARABIC, SCRIPT_HEBREW -> true
- else -> false
- }
- }
}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SpacedTokens.kt b/app/src/main/java/helium314/keyboard/latin/utils/SpacedTokens.kt
deleted file mode 100644
index 01f4c718b..000000000
--- a/app/src/main/java/helium314/keyboard/latin/utils/SpacedTokens.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-
-package helium314.keyboard.latin.utils
-
-/**
- * Tokenizes strings by groupings of non-space characters, making them iterable. Note that letters,
- * punctuations, etc. are all treated the same by this construct.
- */
-class SpacedTokens(phrase: String) : Iterable {
- private val mPhrase = phrase
- private val mLength = phrase.length
- private val mStartPos = phrase.indexOfFirst { !Character.isWhitespace(it) }
- // the iterator should start at the first non-whitespace character
-
- override fun iterator() = object : Iterator {
- private var startPos = mStartPos
-
- override fun hasNext(): Boolean {
- return startPos < mLength && startPos != -1
- }
-
- override fun next(): String {
- var endPos = startPos
-
- do if (++endPos >= mLength) break
- while (!Character.isWhitespace(mPhrase[endPos]))
- val word = mPhrase.substring(startPos, endPos)
-
- if (endPos < mLength) {
- do if (++endPos >= mLength) break
- while (Character.isWhitespace(mPhrase[endPos]))
- }
- startPos = endPos
-
- return word
- }
- }
-}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.java
new file mode 100644
index 000000000..bb99355b6
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+
+package helium314.keyboard.latin.utils;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.inputmethod.InputMethodSubtype;
+
+import helium314.keyboard.compat.ConfigurationCompatKt;
+import helium314.keyboard.latin.R;
+import helium314.keyboard.latin.common.Constants;
+import helium314.keyboard.latin.common.LocaleUtils;
+import helium314.keyboard.latin.common.StringUtils;
+
+import java.util.HashMap;
+import java.util.Locale;
+
+import static helium314.keyboard.latin.common.Constants.Subtype.ExtraValue.COMBINING_RULES;
+import static helium314.keyboard.latin.common.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * A helper class to deal with subtype locales.
+ */
+// TODO: consolidate this into RichInputMethodSubtype
+// todo (later): see whether this complicated mess can be simplified
+public final class SubtypeLocaleUtils {
+ static final String TAG = SubtypeLocaleUtils.class.getSimpleName();
+
+ // This reference class {@link R} must be located in the same package as LatinIME.java.
+ // switched to context.getPackageName(), which works with changed debug package name
+ // any reason to prefer original version?
+// private static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName();
+
+ // Special language code to represent "no language".
+ public static final String NO_LANGUAGE = "zz";
+ public static final String QWERTY = "qwerty";
+ public static final String EMOJI = "emoji";
+ public static final int UNKNOWN_KEYBOARD_LAYOUT = R.string.subtype_generic;
+
+ private static volatile boolean sInitialized = false;
+ private static final Object sInitializeLock = new Object();
+ private static Resources sResources;
+ // Keyboard layout to its display name map.
+ private static final HashMap sKeyboardLayoutToDisplayNameMap = new HashMap<>();
+ // Keyboard layout to subtype name resource id map.
+ private static final HashMap sKeyboardLayoutToNameIdsMap = new HashMap<>();
+ // Exceptional locale whose name should be displayed in Locale.ROOT.
+ private static final HashMap sExceptionalLocaleDisplayedInRootLocale = new HashMap<>();
+ // Exceptional locale to subtype name resource id map.
+ private static final HashMap sExceptionalLocaleToNameIdsMap = new HashMap<>();
+ // Exceptional locale to subtype name with layout resource id map.
+ private static final HashMap sExceptionalLocaleToWithLayoutNameIdsMap = new HashMap<>();
+ private static final HashMap sResourceSubtypeDisplayNames = new HashMap<>();
+ private static final String SUBTYPE_NAME_RESOURCE_PREFIX = "string/subtype_";
+ private static final String SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX = "string/subtype_generic_";
+ private static final String SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX = "string/subtype_with_layout_";
+ private static final String SUBTYPE_NAME_RESOURCE_NO_LANGUAGE_PREFIX = "string/subtype_no_language_";
+ private static final String SUBTYPE_NAME_RESOURCE_IN_ROOT_LOCALE_PREFIX = "string/subtype_in_root_locale_";
+
+ private SubtypeLocaleUtils() {
+ // Intentional empty constructor for utility class.
+ }
+
+ // Note that this initialization method can be called multiple times.
+ public static void init(final Context context) {
+ synchronized (sInitializeLock) {
+ if (!sInitialized) {
+ initLocked(context);
+ sInitialized = true;
+ }
+ }
+ }
+
+ private static void initLocked(final Context context) {
+ final String RESOURCE_PACKAGE_NAME = context.getPackageName();
+ final Resources res = context.getResources();
+ sResources = res;
+
+ final String[] predefinedLayoutSet = res.getStringArray(R.array.predefined_layouts);
+ final String[] layoutDisplayNames = res.getStringArray(R.array.predefined_layout_display_names);
+ for (int i = 0; i < predefinedLayoutSet.length; i++) {
+ final String layoutName = predefinedLayoutSet[i];
+ sKeyboardLayoutToDisplayNameMap.put(layoutName, layoutDisplayNames[i]);
+ final String resourceName = SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX + layoutName;
+ final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME);
+ sKeyboardLayoutToNameIdsMap.put(layoutName, resId);
+ // Register subtype name resource id of "No language" with key "zz_"
+ final String noLanguageResName = SUBTYPE_NAME_RESOURCE_NO_LANGUAGE_PREFIX + layoutName;
+ final int noLanguageResId = res.getIdentifier(noLanguageResName, null, RESOURCE_PACKAGE_NAME);
+ final String key = getNoLanguageLayoutKey(layoutName);
+ sKeyboardLayoutToNameIdsMap.put(key, noLanguageResId);
+ }
+
+ final String[] exceptionalLocaleInRootLocale = res.getStringArray(R.array.subtype_locale_displayed_in_root_locale);
+ for (final String languageTag : exceptionalLocaleInRootLocale) {
+ final String resourceName = SUBTYPE_NAME_RESOURCE_IN_ROOT_LOCALE_PREFIX + languageTag.replace('-', '_');
+ final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME);
+ sExceptionalLocaleDisplayedInRootLocale.put(languageTag, resId);
+ }
+
+ final String[] exceptionalLocales = res.getStringArray(R.array.subtype_locale_exception_keys);
+ for (final String languageTag : exceptionalLocales) {
+ final String resourceName = SUBTYPE_NAME_RESOURCE_PREFIX + languageTag.replace('-', '_');
+ final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME);
+ sExceptionalLocaleToNameIdsMap.put(languageTag, resId);
+ final String resourceNameWithLayout = SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX + languageTag.replace('-', '_');
+ final int resIdWithLayout = res.getIdentifier(resourceNameWithLayout, null, RESOURCE_PACKAGE_NAME);
+ sExceptionalLocaleToWithLayoutNameIdsMap.put(languageTag, resIdWithLayout);
+ }
+ }
+
+ public static boolean isExceptionalLocale(final Locale locale) {
+ return sExceptionalLocaleToNameIdsMap.containsKey(locale.toLanguageTag());
+ }
+
+ private static String getNoLanguageLayoutKey(final String keyboardLayoutName) {
+ return NO_LANGUAGE + "_" + keyboardLayoutName;
+ }
+
+ public static int getSubtypeNameResId(final Locale locale, final String keyboardLayoutName) {
+ final String languageTag = locale.toLanguageTag();
+ if (isExceptionalLocale(locale)) {
+ return sExceptionalLocaleToWithLayoutNameIdsMap.get(languageTag);
+ }
+ final String key = NO_LANGUAGE.equals(languageTag)
+ ? getNoLanguageLayoutKey(keyboardLayoutName)
+ : keyboardLayoutName;
+ final Integer nameId = sKeyboardLayoutToNameIdsMap.get(key);
+ return nameId == null ? UNKNOWN_KEYBOARD_LAYOUT : nameId;
+ }
+
+ @NonNull
+ public static Locale getDisplayLocaleOfSubtypeLocale(@NonNull final Locale locale) {
+ final String languageTag = locale.toLanguageTag();
+ if (NO_LANGUAGE.equals(languageTag)) {
+ return ConfigurationCompatKt.locale(sResources.getConfiguration());
+ }
+ if (sExceptionalLocaleDisplayedInRootLocale.containsKey(languageTag)) {
+ return Locale.ROOT;
+ }
+ return locale;
+ }
+
+ public static String getSubtypeLocaleDisplayNameInSystemLocale(@NonNull final Locale locale) {
+ final Locale displayLocale = ConfigurationCompatKt.locale(sResources.getConfiguration());
+ return getSubtypeLocaleDisplayNameInternal(locale, displayLocale);
+ }
+
+ @NonNull
+ public static String getSubtypeLocaleDisplayName(@NonNull final Locale locale) {
+ final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(locale);
+ return getSubtypeLocaleDisplayNameInternal(locale, displayLocale);
+ }
+
+ @NonNull
+ public static String getSubtypeLanguageDisplayName(@NonNull final Locale locale) {
+ final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(locale);
+ final Locale languageLocale;
+ if (sExceptionalLocaleDisplayedInRootLocale.containsKey(locale.toLanguageTag())) {
+ languageLocale = locale;
+ } else {
+ languageLocale = LocaleUtils.constructLocale(locale.getLanguage());
+ }
+ return getSubtypeLocaleDisplayNameInternal(languageLocale, displayLocale);
+ }
+
+ @NonNull
+ private static String getSubtypeLocaleDisplayNameInternal(@NonNull final Locale locale,
+ @NonNull final Locale displayLocale) {
+ final String languageTag = locale.toLanguageTag();
+ if (NO_LANGUAGE.equals(locale.toLanguageTag())) {
+ // No language subtype should be displayed in system locale.
+ return sResources.getString(R.string.subtype_no_language);
+ }
+ final Integer exceptionalNameResId;
+ if (displayLocale.equals(Locale.ROOT)
+ && sExceptionalLocaleDisplayedInRootLocale.containsKey(languageTag)) {
+ exceptionalNameResId = sExceptionalLocaleDisplayedInRootLocale.get(languageTag);
+ } else if (sExceptionalLocaleToNameIdsMap.containsKey(languageTag)) {
+ exceptionalNameResId = sExceptionalLocaleToNameIdsMap.get(languageTag);
+ } else {
+ exceptionalNameResId = null;
+ }
+
+ final String displayName;
+ if (exceptionalNameResId != null) {
+ displayName = RunInLocaleKt.runInLocale(sResources, displayLocale, res -> res.getString(exceptionalNameResId));
+ } else {
+ displayName = LocaleUtils.getLocaleDisplayNameInLocale(locale, sResources, displayLocale);
+ }
+ return StringUtils.capitalizeFirstCodePoint(displayName, displayLocale);
+ }
+
+ // InputMethodSubtype's display name in its locale.
+ // isAdditionalSubtype (T=true, F=false)
+ // locale layout | display name
+ // ------ ------- - ----------------------
+ // en_US qwerty F English (US) exception
+ // en_GB qwerty F English (UK) exception
+ // es_US spanish F Español (EE.UU.) exception
+ // fr azerty F Français
+ // fr_CA qwerty F Français (Canada)
+ // fr_CH swiss F Français (Suisse)
+ // de qwertz F Deutsch
+ // de_CH swiss T Deutsch (Schweiz)
+ // zz qwerty F Alphabet (QWERTY) in system locale
+ // fr qwertz T Français (QWERTZ)
+ // de qwerty T Deutsch (QWERTY)
+ // en_US azerty T English (US) (AZERTY) exception
+ // zz azerty T Alphabet (AZERTY) in system locale
+
+ @NonNull
+ private static String getReplacementString(@NonNull final InputMethodSubtype subtype,
+ @NonNull final Locale displayLocale) {
+ if (subtype.containsExtraValueKey(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)) {
+ return subtype.getExtraValueOf(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME);
+ }
+ return getSubtypeLocaleDisplayNameInternal(SubtypeUtilsKt.locale(subtype), displayLocale);
+ }
+
+ @NonNull
+ public static String getDisplayNameInSystemLocale(@NonNull final String mainLayoutName, @NonNull final Locale locale) {
+ final String displayName = getMainLayoutDisplayName(mainLayoutName);
+ if (displayName != null) // works for custom and latin layouts
+ return displayName;
+ // we have some locale-specific layout
+ for (InputMethodSubtype subtype : SubtypeSettings.INSTANCE.getResourceSubtypesForLocale(locale)) {
+ final String main = LayoutType.Companion.getMainLayoutFromExtraValue(subtype.getExtraValue());
+ if (mainLayoutName.equals(main))
+ return getSubtypeDisplayNameInSystemLocale(subtype);
+ }
+ return mainLayoutName; // should never happen...
+ }
+
+ @NonNull
+ public static String getSubtypeDisplayNameInSystemLocale(@NonNull final InputMethodSubtype subtype) {
+ final String cached = sResourceSubtypeDisplayNames.get(subtype.hashCode());
+ if (cached != null) return cached;
+
+ final Locale displayLocale = ConfigurationCompatKt.locale(sResources.getConfiguration());
+ final String displayName = getSubtypeDisplayNameInternal(subtype, displayLocale);
+
+ if (!subtype.containsExtraValueKey(Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE)) {
+ sResourceSubtypeDisplayNames.put(subtype.hashCode(), displayName);
+ }
+ return displayName;
+ }
+
+ public static void clearDisplayNameCache() {
+ sResourceSubtypeDisplayNames.clear();
+ }
+
+ @NonNull
+ public static String getSubtypeNameForLogging(@Nullable final InputMethodSubtype subtype) {
+ if (subtype == null) {
+ return "";
+ }
+ return SubtypeUtilsKt.locale(subtype) + "/" + getMainLayoutName(subtype);
+ }
+
+ @NonNull
+ private static String getSubtypeDisplayNameInternal(@NonNull final InputMethodSubtype subtype,
+ @NonNull final Locale displayLocale) {
+ final String replacementString = getReplacementString(subtype, displayLocale);
+ final int nameResId = subtype.getNameResId();
+ return RunInLocaleKt.runInLocale(sResources, displayLocale,
+ res -> {
+ try {
+ return StringUtils.capitalizeFirstCodePoint(res.getString(nameResId, replacementString), displayLocale);
+ } catch (Resources.NotFoundException e) {
+ Log.w(TAG, "Unknown subtype: mode=" + subtype.getMode()
+ + " nameResId=" + subtype.getNameResId()
+ + " locale=" + subtype.getLocale()
+ + " extra=" + subtype.getExtraValue()
+ + "\n" + DebugLogUtils.getStackTrace());
+ return "";
+ }
+ });
+ }
+
+ @Nullable
+ public static String getMainLayoutDisplayName(@NonNull final InputMethodSubtype subtype) {
+ final String layoutName = getMainLayoutName(subtype);
+ return getMainLayoutDisplayName(layoutName);
+ }
+
+ @Nullable
+ public static String getMainLayoutDisplayName(@NonNull final String layoutName) {
+ if (LayoutUtilsCustom.INSTANCE.isCustomLayout(layoutName))
+ return LayoutUtilsCustom.INSTANCE.getDisplayName(layoutName);
+ return sKeyboardLayoutToDisplayNameMap.get(layoutName);
+ }
+
+ @NonNull
+ public static String getMainLayoutName(final InputMethodSubtype subtype) {
+ String mainLayoutName = SubtypeUtilsKt.mainLayoutName(subtype);
+ if (mainLayoutName == null && subtype.isAsciiCapable()) {
+ mainLayoutName = QWERTY;
+ }
+ if (mainLayoutName == null) { // we could search for a subtype with the correct script, but this is a bug anyway...
+ Log.w(TAG, "KeyboardLayoutSet not found, use QWERTY: " +
+ "locale=" + subtype.getLocale() + " extraValue=" + subtype.getExtraValue());
+ return QWERTY;
+ }
+ return mainLayoutName;
+ }
+
+ public static String getCombiningRulesExtraValue(final InputMethodSubtype subtype) {
+ return subtype.getExtraValueOf(COMBINING_RULES);
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.kt
deleted file mode 100644
index 0861bf952..000000000
--- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.kt
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- * modified
- * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
- */
-package helium314.keyboard.latin.utils
-
-import android.content.Context
-import android.content.res.Resources
-import android.view.inputmethod.InputMethodSubtype
-import helium314.keyboard.compat.locale
-import helium314.keyboard.latin.R
-import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue
-import helium314.keyboard.latin.common.LocaleUtils.constructLocale
-import helium314.keyboard.latin.common.LocaleUtils.localizedDisplayName
-import helium314.keyboard.latin.common.StringUtils
-import helium314.keyboard.latin.utils.LayoutType.Companion.getMainLayoutFromExtraValue
-import java.util.Locale
-import kotlin.concurrent.Volatile
-
-/**
- * A helper class to deal with subtype locales.
- */
-object SubtypeLocaleUtils {
- @Volatile
- private var initialized = false
- private lateinit var resources: Resources
-
- // Keyboard layout to its display name map.
- private val keyboardLayoutToDisplayName = HashMap()
-
- // Exceptional locale whose name should be displayed in Locale.ROOT.
- private val exceptionalLocaleDisplayedInRootLocale = HashMap()
-
- private val resourceSubtypeDisplayNameCache = HashMap()
-
- // Note that this initialization method can be called multiple times.
- @JvmStatic
- fun init(context: Context) {
- synchronized(this) {
- if (!initialized) {
- initLocked(context)
- initialized = true
- }
- }
- }
-
- private fun initLocked(context: Context) {
- resources = context.resources
-
- // todo: layout names are currently translatable in subtype_no_language_* but not the default names
- // just remove the separate "alphabet ()" strings and have a single "alphabet (%s)"?
- // or rather use the same style as for languages and only have "alphabet"
- val predefinedLayouts = resources.getStringArray(R.array.predefined_layouts)
- val layoutDisplayNames = resources.getStringArray(R.array.predefined_layout_display_names)
- for (i in predefinedLayouts.indices) {
- val layoutName = predefinedLayouts[i]
- keyboardLayoutToDisplayName[layoutName] = layoutDisplayNames[i]
- }
-
- val exceptionalLocaleInRootLocale = resources.getStringArray(R.array.subtype_locale_displayed_in_root_locale)
- val exceptionalLocaleInRootLocaleDisplayNames = resources.getStringArray(R.array.subtype_locale_displayed_in_root_locale_display_names)
- for (i in exceptionalLocaleInRootLocale.indices) {
- exceptionalLocaleDisplayedInRootLocale[exceptionalLocaleInRootLocale[i]] = exceptionalLocaleInRootLocaleDisplayNames[i]
- }
- }
-
- // see SubtypeUtilsAdditional.getAdditionalExtraValues, currently not needed
- //fun isExceptionalLocale(locale: Locale) = exceptionalLocaleDisplayedInRootLocale.containsKey(locale.toLanguageTag())
-
- /** Usually the [locale], but Locale.ROOT for exceptionalLocaleDisplayedInRootLocale, and system locale for NO_LANGUAGE */
- private fun getDisplayLocaleOfSubtypeLocale(locale: Locale): Locale {
- val languageTag = locale.toLanguageTag()
- if (languageTag == NO_LANGUAGE)
- return resources.configuration.locale()
- if (exceptionalLocaleDisplayedInRootLocale.containsKey(languageTag))
- return Locale.ROOT
- return locale
- }
-
- /** Returns the full locale display name for use on space bar (considers exceptionalLocaleDisplayedInRootLocale) */
- fun getSubtypeLocaleDisplayName(locale: Locale): String {
- val displayLocale = getDisplayLocaleOfSubtypeLocale(locale)
- return getSubtypeLocaleDisplayNameInternal(locale, displayLocale)
- }
-
- /** Returns the language display name for use on space bar (considers exceptionalLocaleDisplayedInRootLocale) */
- fun getSubtypeLanguageDisplayName(locale: Locale): String {
- val languageLocale = if (exceptionalLocaleDisplayedInRootLocale.containsKey(locale.toLanguageTag()))
- locale
- else
- locale.language.constructLocale()
- return getSubtypeLocaleDisplayNameInternal(languageLocale, getDisplayLocaleOfSubtypeLocale(locale))
- }
-
- /**
- * Display name of subtype [locale] in [displayLocale].
- * Considers exceptionalLocaleDisplayedInRootLocale and exceptionalLocaleToNameIds, defaults to Locale.localizedDisplayName.
- */
- private fun getSubtypeLocaleDisplayNameInternal(locale: Locale, displayLocale: Locale): String {
- val languageTag = locale.toLanguageTag()
- if (languageTag == NO_LANGUAGE) {
- // "No language" subtype should be displayed in system locale.
- return resources.getString(R.string.subtype_no_language)
- }
- val displayName = if (displayLocale == Locale.ROOT && exceptionalLocaleDisplayedInRootLocale.containsKey(languageTag)) {
- exceptionalLocaleDisplayedInRootLocale[languageTag]!!
- } else {
- locale.localizedDisplayName(resources, displayLocale)
- }
- return StringUtils.capitalizeFirstCodePoint(displayName, displayLocale)
- }
-
- @JvmStatic
- fun clearSubtypeDisplayNameCache() {
- resourceSubtypeDisplayNameCache.clear()
- }
-
- @JvmStatic
- fun getSubtypeNameForLogging(subtype: InputMethodSubtype?): String {
- if (subtype == null) {
- return ""
- }
- return subtype.locale().toString() + "/" + subtype.mainLayoutNameOrQwerty()
- }
-
- /**
- * Subtype display name is when using the default layout, or () otherwise
- * [displayLocale] defaults to system locale
- */
- fun InputMethodSubtype.displayName(displayLocale: Locale? = null): String {
- if (displayLocale == null) resourceSubtypeDisplayNameCache[hashCode()]?.let { return it }
-
- val layoutName = mainLayoutName()
- if (layoutName != null && LayoutUtilsCustom.isCustomLayout(layoutName)) {
- return resources.getString(
- R.string.subtype_with_layout_generic,
- locale().localizedDisplayName(resources, displayLocale),
- LayoutUtilsCustom.getDisplayName(layoutName)
- )
- }
- // if it's a default layout, we want to use the nameResId instead of explicitly showing the layout name
- if (keyboardLayoutToDisplayName.containsKey(layoutName)
- && SubtypeSettings.getResourceSubtypesForLocale(locale()).none { it.mainLayoutName() == layoutName }) {
- return resources.getString(
- R.string.subtype_with_layout_generic,
- locale().localizedDisplayName(resources, displayLocale),
- keyboardLayoutToDisplayName[layoutName]
- )
- }
-
- val actualDisplayLocale = displayLocale ?: resources.configuration.locale()
- // replacement for %s in nameResId, which now always is the locale
- // not read from ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME any more
- val replacementString = getSubtypeLocaleDisplayNameInternal(locale(), actualDisplayLocale)
-
- val name = runCatching {
- if (displayLocale == null) resources.getString(nameResId, replacementString)
- else runInLocale(resources, displayLocale) { resources.getString(nameResId, replacementString) }
- }.getOrNull() ?: locale().localizedDisplayName(resources, displayLocale)
- val displayName = StringUtils.capitalizeFirstCodePoint(name, actualDisplayLocale)
- if (displayLocale == null && !containsExtraValueKey(ExtraValue.IS_ADDITIONAL_SUBTYPE))
- resourceSubtypeDisplayNameCache[hashCode()] = displayName
- return displayName
- }
-
- fun getMainLayoutDisplayName(layoutName: String): String? =
- if (LayoutUtilsCustom.isCustomLayout(layoutName)) LayoutUtilsCustom.getDisplayName(layoutName)
- else keyboardLayoutToDisplayName[layoutName]
-
- fun getLayoutDisplayNameInSystemLocale(mainLayoutName: String, locale: Locale): String {
- getMainLayoutDisplayName(mainLayoutName)?.let { return it } // works for custom and latin layouts
-
- // we have some locale-specific layout, use the subtype name
- for (subtype in SubtypeSettings.getResourceSubtypesForLocale(locale)) {
- if (mainLayoutName == getMainLayoutFromExtraValue(subtype.extraValue))
- return subtype.displayName()
- }
- return mainLayoutName // should never happen...
- }
-
- @JvmStatic
- fun getCombiningRulesExtraValue(subtype: InputMethodSubtype): String? = subtype.getExtraValueOf(ExtraValue.COMBINING_RULES)
-
- // Special language code to represent "no language".
- const val NO_LANGUAGE = "zz"
- const val QWERTY = "qwerty"
- const val EMOJI = "emoji"
- val UNKNOWN_KEYBOARD_LAYOUT = R.string.subtype_generic
-}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt
index 00a9e752d..88a5d9450 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt
@@ -28,11 +28,9 @@ object SubtypeSettings {
fun getEnabledSubtypes(fallback: Boolean = false): List {
if (fallback && enabledSubtypes.isEmpty())
return getDefaultEnabledSubtypes()
- return enabledSubtypes
+ return enabledSubtypes.toList()
}
- fun isEnabled(subtype: InputMethodSubtype?): Boolean = subtype in enabledSubtypes || subtype in getDefaultEnabledSubtypes()
-
fun getAllAvailableSubtypes(): List =
resourceSubtypesByLocale.values.flatten() + additionalSubtypes
@@ -156,7 +154,7 @@ object SubtypeSettings {
}
else subtype.toPref()
}.joinToString(Separators.SETS)
- editor.putString(key, new)
+ prefs.edit().putString(key, new).apply()
}
editor.apply()
if (Settings.readDefaultLayoutName(type, prefs) == from)
@@ -166,10 +164,10 @@ object SubtypeSettings {
fun reloadEnabledSubtypes(context: Context) {
enabledSubtypes.clear()
+ removeInvalidCustomSubtypes(context)
loadAdditionalSubtypes(context.prefs())
loadEnabledSubtypes(context)
- if (RichInputMethodManager.isInitialized())
- RichInputMethodManager.getInstance().refreshSubtypeCaches()
+ RichInputMethodManager.getInstance().refreshSubtypeCaches()
}
fun createSettingsSubtypes(prefSubtypes: String): List =
@@ -189,6 +187,7 @@ object SubtypeSettings {
reloadSystemLocales(context)
loadResourceSubtypes(context.resources)
+ removeInvalidCustomSubtypes(context)
loadAdditionalSubtypes(context.prefs())
loadEnabledSubtypes(context)
}
@@ -216,6 +215,24 @@ object SubtypeSettings {
}
}
+ // remove custom subtypes without a layout file
+ private fun removeInvalidCustomSubtypes(context: Context) {
+ val prefs = context.prefs()
+ val additionalSubtypes = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!.split(Separators.SETS)
+ val customLayoutFiles by lazy { LayoutUtilsCustom.getLayoutFiles(LayoutType.MAIN, context).map { it.name } }
+ val subtypesToRemove = mutableListOf()
+ additionalSubtypes.forEach {
+ val name = it.toSettingsSubtype().mainLayoutName() ?: SubtypeLocaleUtils.QWERTY
+ if (!LayoutUtilsCustom.isCustomLayout(name)) return@forEach
+ if (name !in customLayoutFiles)
+ subtypesToRemove.add(it)
+ }
+ if (subtypesToRemove.isEmpty()) return
+ Log.w(TAG, "removing custom subtypes without main layout files: $subtypesToRemove")
+ // todo: now we have a qwerty fallback anyway, consider removing this method (makes bugs more obvious to users)
+ prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, additionalSubtypes.filterNot { it in subtypesToRemove }.joinToString(Separators.SETS)).apply()
+ }
+
private fun loadAdditionalSubtypes(prefs: SharedPreferences) {
additionalSubtypes.clear()
val additionalSubtypeString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
@@ -243,7 +260,7 @@ object SubtypeSettings {
continue
}
- val subtype = subtypesForLocale.firstOrNull { it.mainLayoutNameOrQwerty() == (settingsSubtype.mainLayoutName() ?: SubtypeLocaleUtils.QWERTY) }
+ val subtype = subtypesForLocale.firstOrNull { SubtypeLocaleUtils.getMainLayoutName(it) == (settingsSubtype.mainLayoutName() ?: SubtypeLocaleUtils.QWERTY) }
if (subtype == null) {
val message = "subtype $settingsSubtype could not be loaded"
Log.w(TAG, message)
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt
index 1b69075c3..a210174ac 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt
@@ -1,5 +1,6 @@
package helium314.keyboard.latin.utils
+import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import android.os.Build
@@ -9,6 +10,7 @@ import helium314.keyboard.latin.common.Constants.Separators
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
+import helium314.keyboard.latin.common.LocaleUtils.localizedDisplayName
import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.settings.SettingsSubtype.Companion.getExtraValueOf
@@ -28,8 +30,6 @@ fun InputMethodSubtype.mainLayoutName(): String? {
return map[LayoutType.MAIN]
}
-fun InputMethodSubtype.mainLayoutNameOrQwerty(): String = mainLayoutName() ?: SubtypeLocaleUtils.QWERTY
-
fun getResourceSubtypes(resources: Resources): List {
val subtypes = mutableListOf()
val xml = resources.getXml(R.xml.method)
@@ -64,6 +64,15 @@ fun getResourceSubtypes(resources: Resources): List {
return subtypes
}
+/** Workaround for SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale ignoring custom layout names */
+// todo (later): this should be done properly and in SubtypeLocaleUtils
+fun InputMethodSubtype.displayName(context: Context): String {
+ val layoutName = SubtypeLocaleUtils.getMainLayoutName(this)
+ if (LayoutUtilsCustom.isCustomLayout(layoutName))
+ return "${locale().localizedDisplayName(context)} (${LayoutUtilsCustom.getDisplayName(layoutName)})"
+ return SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(this)
+}
+
fun getHasLocalizedNumberRow(subtype: InputMethodSubtype, prefs: SharedPreferences): Boolean =
subtype.getExtraValueOf(ExtraValue.LOCALIZED_NUMBER_ROW)?.toBoolean()
?: prefs.getBoolean(Settings.PREF_LOCALIZED_NUMBER_ROW, Defaults.PREF_LOCALIZED_NUMBER_ROW)
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt
index a2d839266..95875bac2 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt
@@ -9,13 +9,9 @@ import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.Constants.Separators
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue
import helium314.keyboard.latin.settings.Defaults
-import helium314.keyboard.latin.settings.Defaults.default
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.settings.SettingsSubtype
import helium314.keyboard.latin.settings.SettingsSubtype.Companion.toSettingsSubtype
-import helium314.keyboard.latin.utils.LayoutType.Companion.toExtraValue
-import helium314.keyboard.latin.utils.ScriptUtils.script
-import java.util.EnumMap
import java.util.Locale
object SubtypeUtilsAdditional {
@@ -50,18 +46,9 @@ object SubtypeUtilsAdditional {
fun createDummyAdditionalSubtype(locale: Locale, mainLayoutName: String) =
createAdditionalSubtype(locale, "${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN${Separators.KV}$mainLayoutName", false, false)
- // only used in tests
fun createEmojiCapableAdditionalSubtype(locale: Locale, mainLayoutName: String, asciiCapable: Boolean) =
createAdditionalSubtype(locale, "${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN${Separators.KV}$mainLayoutName", asciiCapable, true)
- /** creates a subtype with every layout being the default for its type */
- fun createDefaultSubtype(locale: Locale): InputMethodSubtype {
- val layouts = LayoutType.entries.associateWithTo(LayoutType.getLayoutMap(null)) { it.default }
- SubtypeSettings.getResourceSubtypesForLocale(locale).firstOrNull()?.mainLayoutName()?.let { layouts[LayoutType.MAIN] = it }
- val extra = ExtraValue.KEYBOARD_LAYOUT_SET + "=" + layouts.toExtraValue()
- return createAdditionalSubtype(locale, extra, locale.script() == ScriptUtils.SCRIPT_LATIN, true)
- }
-
fun removeAdditionalSubtype(context: Context, subtype: InputMethodSubtype) {
val prefs = context.prefs()
SubtypeSettings.removeEnabledSubtype(context, subtype)
@@ -113,6 +100,8 @@ object SubtypeUtilsAdditional {
}
private fun getNameResId(locale: Locale, mainLayoutName: String): Int {
+ val nameId = SubtypeLocaleUtils.getSubtypeNameResId(locale, mainLayoutName)
+ if (nameId != SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) return nameId
SubtypeSettings.getResourceSubtypesForLocale(locale).forEach {
if (it.mainLayoutName() == mainLayoutName) return it.nameResId
}
@@ -161,17 +150,15 @@ object SubtypeUtilsAdditional {
val extraValueItems = mutableListOf()
if (isAsciiCapable)
extraValueItems.add(ExtraValue.ASCII_CAPABLE)
-/* if (SubtypeLocaleUtils.isExceptionalLocale(locale)) {
+ if (SubtypeLocaleUtils.isExceptionalLocale(locale)) {
// this seems to be for shorter names (e.g. English (US) instead English (United States))
// but is now also used for languages that are not known by Android (at least older versions)
// todo: actually this should never contain a custom layout name, because it may contain any
// characters including , and = which may break extra values
- // todo: disabled for now, not necessary with the more generic subtype name
- // might become necessary again when exposing subtypes to the system
extraValueItems.add(
ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + SubtypeLocaleUtils.getMainLayoutDisplayName(mainLayoutName)
)
- }*/
+ }
if (isEmojiCapable)
extraValueItems.add(ExtraValue.EMOJI_CAPABLE)
extraValueItems.add(ExtraValue.IS_ADDITIONAL_SUBTYPE)
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/TextRange.java b/app/src/main/java/helium314/keyboard/latin/utils/TextRange.java
index 1a0e81fdf..659e01a95 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/TextRange.java
+++ b/app/src/main/java/helium314/keyboard/latin/utils/TextRange.java
@@ -7,13 +7,9 @@
package helium314.keyboard.latin.utils;
import android.text.Spanned;
-import android.text.TextUtils;
import android.text.style.SuggestionSpan;
-import androidx.annotation.NonNull;
-
import java.util.Arrays;
-import java.util.Objects;
/**
* Represents a range of text, relative to the current cursor position.
@@ -99,28 +95,6 @@ public final class TextRange {
return writeIndex == readIndex ? spans : Arrays.copyOfRange(spans, 0, writeIndex);
}
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof TextRange textRange)) return false;
- return mWordAtCursorStartIndex == textRange.mWordAtCursorStartIndex
- && mWordAtCursorEndIndex == textRange.mWordAtCursorEndIndex
- && mCursorIndex == textRange.mCursorIndex
- && mHasUrlSpans == textRange.mHasUrlSpans
- && TextUtils.equals(mTextAtCursor, textRange.mTextAtCursor)
- && TextUtils.equals(mWord, textRange.mWord);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mTextAtCursor, mWordAtCursorStartIndex, mWordAtCursorEndIndex, mCursorIndex, mWord, mHasUrlSpans);
- }
-
- @NonNull
- @Override
- public String toString() {
- return mTextAtCursor + ", " + mWord + ", " + mCursorIndex;
- }
-
public TextRange(final CharSequence textAtCursor, final int wordAtCursorStartIndex,
final int wordAtCursorEndIndex, final int cursorIndex, final boolean hasUrlSpans) {
if (wordAtCursorStartIndex < 0 || cursorIndex < wordAtCursorStartIndex
@@ -135,4 +109,4 @@ public final class TextRange {
mHasUrlSpans = hasUrlSpans;
mWord = mTextAtCursor.subSequence(mWordAtCursorStartIndex, mWordAtCursorEndIndex);
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt
index d9428b1f7..1293d33a4 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt
@@ -21,7 +21,7 @@ import kotlinx.coroutines.launch
import java.util.EnumMap
import java.util.Locale
-fun createToolbarKey(context: Context, key: ToolbarKey): ImageButton {
+fun createToolbarKey(context: Context, iconsSet: KeyboardIconsSet, key: ToolbarKey): ImageButton {
val button = ImageButton(context, null, R.attr.suggestionWordStyle)
button.scaleType = ImageView.ScaleType.CENTER
button.tag = key
@@ -29,7 +29,7 @@ fun createToolbarKey(context: Context, key: ToolbarKey): ImageButton {
if (contentDescriptionId != 0)
button.contentDescription = context.getString(contentDescriptionId)
setToolbarButtonActivatedState(button)
- button.setImageDrawable(KeyboardIconsSet.instance.getNewDrawable(key.name, context))
+ button.setImageDrawable(iconsSet.getNewDrawable(key.name, context))
return button
}
@@ -115,10 +115,6 @@ enum class ToolbarKey {
PAGE_UP, PAGE_DOWN, FULL_LEFT, FULL_RIGHT, PAGE_START, PAGE_END
}
-enum class ToolbarMode {
- EXPANDABLE, TOOLBAR_KEYS, SUGGESTION_STRIP, HIDDEN,
-}
-
val toolbarKeyStrings = entries.associateWithTo(EnumMap(ToolbarKey::class.java)) { it.toString().lowercase(Locale.US) }
val defaultToolbarPref by lazy {
diff --git a/app/src/main/java/helium314/keyboard/settings/Icons.kt b/app/src/main/java/helium314/keyboard/settings/Icons.kt
index 4bd04bbe6..676af9aa1 100644
--- a/app/src/main/java/helium314/keyboard/settings/Icons.kt
+++ b/app/src/main/java/helium314/keyboard/settings/Icons.kt
@@ -11,19 +11,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
-import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.LayoutDirection
import helium314.keyboard.latin.R
@Composable
fun NextScreenIcon() {
- Icon(
- painterResource(R.drawable.ic_arrow_left), null,
- if (LocalLayoutDirection.current == LayoutDirection.Ltr) Modifier.scale(-1f, 1f) else Modifier
- )
+ Icon(painterResource(R.drawable.ic_arrow_left), null, Modifier.scale(-1f, 1f))
}
@Composable
diff --git a/app/src/main/java/helium314/keyboard/settings/Misc.kt b/app/src/main/java/helium314/keyboard/settings/Misc.kt
index fffc7ba1f..2ca4add2c 100644
--- a/app/src/main/java/helium314/keyboard/settings/Misc.kt
+++ b/app/src/main/java/helium314/keyboard/settings/Misc.kt
@@ -8,9 +8,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.DropdownMenu
@@ -46,25 +43,6 @@ fun WithSmallTitle(
}
}
-@Composable
-fun ActionRow(
- modifier: Modifier = Modifier,
- onClick: (() -> Unit)? = null,
- verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
- content: @Composable RowScope.() -> Unit
-) {
- val clickableModifier = if (onClick != null) Modifier.clickable(onClick = onClick)
- else Modifier
- Row(
- modifier = modifier
- .then(clickableModifier)
- .fillMaxWidth()
- .heightIn(min = 44.dp),
- verticalAlignment = verticalAlignment,
- content = content
- )
-}
-
/** Icon if resource is a vector image, (bitmap) Image otherwise */
@Composable
fun IconOrImage(@DrawableRes resId: Int, name: String?, sizeDp: Float) {
diff --git a/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt b/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
index 16b2e2d92..8fd287bf7 100644
--- a/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
@@ -6,17 +6,12 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@@ -25,7 +20,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
@@ -46,7 +40,6 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import helium314.keyboard.latin.R
@@ -65,42 +58,36 @@ fun SearchSettingsScreen(
content = {
if (content != null) content()
else {
- Scaffold(
- contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)
- ) { innerPadding ->
- Column(
- Modifier.verticalScroll(rememberScrollState()).then(Modifier.padding(innerPadding))
- ) {
- settings.forEach {
- if (it is Int) {
- PreferenceCategory(stringResource(it))
- } else {
- // this only animates appearing prefs
- // a solution would be using a list(visible to key)
- AnimatedVisibility(visible = it != null) {
- if (it != null)
- SettingsActivity.settingsContainer[it]?.Preference()
- }
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ settings.forEach {
+ if (it is Int) {
+ PreferenceCategory(stringResource(it))
+ } else {
+ // this only animates appearing prefs
+ // a solution would be using a list(visible to key)
+ AnimatedVisibility(visible = it != null) {
+ if (it != null)
+ SettingsActivity.settingsContainer[it]?.Preference()
}
}
}
- // lazyColumn has janky scroll for a while (not sure why compose gets smoother after a while)
- // maybe related to unnecessary recompositions? but even for just displaying text it's there
- // didn't manage to improve things with @Immutable list wrapper and other lazy list hints
- // so for now: just use "normal" Column
- // even though it takes up to ~50% longer to load it's much better UX
- // and the missing appear animations could be added
- // LazyColumn {
- // items(prefs.filterNotNull(), key = { it }) {
- // Box(Modifier.animateItem()) {
- // if (it is Int)
- // PreferenceCategory(stringResource(it))
- // else
- // SettingsActivity.settingsContainer[it]!!.Preference()
- // }
- // }
- // }
}
+ // lazyColumn has janky scroll for a while (not sure why compose gets smoother after a while)
+ // maybe related to unnecessary recompositions? but even for just displaying text it's there
+ // didn't manage to improve things with @Immutable list wrapper and other lazy list hints
+ // so for now: just use "normal" Column
+ // even though it takes up to ~50% longer to load it's much better UX
+ // and the missing appear animations could be added
+// LazyColumn {
+// items(prefs.filterNotNull(), key = { it }) {
+// Box(Modifier.animateItem()) {
+// if (it is Int)
+// PreferenceCategory(stringResource(it))
+// else
+// SettingsActivity.settingsContainer[it]!!.Preference()
+// }
+// }
+// }
}
},
filteredItems = { SettingsActivity.settingsContainer.filter(it) },
@@ -124,90 +111,93 @@ fun SearchScreen(
// keyboard in unexpected situations such as going back from another screen, which is rather annoying
var searchText by remember { mutableStateOf(TextFieldValue()) }
var showSearch by remember { mutableStateOf(false) }
- Scaffold(contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top))
- { innerPadding ->
- Column(Modifier.fillMaxSize().padding(innerPadding)) {
+ Column(Modifier.fillMaxSize()) {
- fun setShowSearch(value: Boolean) {
- showSearch = value
- if (!value) searchText = TextFieldValue()
- }
- BackHandler {
- if (showSearch || searchText.text.isNotEmpty()) setShowSearch(false)
- else onClickBack()
- }
- Surface(
- color = MaterialTheme.colorScheme.surfaceContainer,
- ) {
- Column {
- TopAppBar(
- title = title,
- windowInsets = WindowInsets(0),
- navigationIcon = {
- IconButton(onClick = {
- if (showSearch) setShowSearch(false)
- else onClickBack()
- }) {
- Icon(
- painterResource(R.drawable.ic_arrow_back),
- stringResource(R.string.spoken_description_action_previous)
- )
- }
- },
- actions = {
- if (icon == null)
- IconButton(onClick = { setShowSearch(!showSearch) }) { SearchIcon() }
- else
- icon()
- if (menu != null)
- Box {
- var showMenu by remember { mutableStateOf(false) }
- IconButton(
- onClick = { showMenu = true }
- ) { Icon(painterResource(R.drawable.ic_arrow_left), "menu", Modifier.rotate(-90f)) }
- DropdownMenu(
- expanded = showMenu,
- onDismissRequest = { showMenu = false }
- ) {
- menu.forEach {
- DropdownMenuItem(
- text = { Text(it.first) },
- onClick = { showMenu = false; it.second() }
- )
- }
+ fun setShowSearch(value: Boolean) {
+ showSearch = value
+ if (!value) searchText = TextFieldValue()
+ }
+ BackHandler {
+ if (showSearch || searchText.text.isNotEmpty()) setShowSearch(false)
+ else onClickBack()
+ }
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ ) {
+ Column {
+ TopAppBar(
+ title = title,
+ windowInsets = TopAppBarDefaults.windowInsets,
+ navigationIcon = {
+ IconButton(onClick = {
+ if (showSearch) setShowSearch(false)
+ else onClickBack()
+ }) {
+ Icon(
+ painterResource(R.drawable.ic_arrow_back),
+ stringResource(R.string.spoken_description_action_previous)
+ )
+ }
+ },
+ actions = {
+ if (icon == null)
+ IconButton(onClick = { setShowSearch(!showSearch) }) { SearchIcon() }
+ else
+ icon()
+ if (menu != null)
+ Box {
+ var showMenu by remember { mutableStateOf(false) }
+ IconButton(
+ onClick = { showMenu = true }
+ ) { Icon(painterResource(R.drawable.ic_arrow_left), "menu", Modifier.rotate(-90f)) }
+ DropdownMenu(
+ expanded = showMenu,
+ onDismissRequest = { showMenu = false }
+ ) {
+ menu.forEach {
+ DropdownMenuItem(
+ text = { Text(it.first) },
+ onClick = { showMenu = false; it.second() }
+ )
}
}
- },
- )
- ExpandableSearchField(
- expanded = showSearch,
- onDismiss = { setShowSearch(false) },
- search = searchText,
- onSearchChange = { searchText = it },
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp),
- colors = TextFieldDefaults.colors(
- focusedContainerColor = MaterialTheme.colorScheme.surface
- )
- )
- }
- }
- CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
- if (searchText.text.isBlank() && content != null) {
- Column {
- content()
- }
- } else {
- val items = filteredItems(searchText.text)
- Scaffold(
- contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)
- ) { innerPadding ->
- LazyColumn(contentPadding = innerPadding) {
- items(items) {
- itemContent(it)
}
- }
+ },
+ )
+ ExpandableSearchField(
+ expanded = showSearch,
+ onDismiss = { setShowSearch(false) },
+ search = searchText,
+ onSearchChange = { searchText = it },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = MaterialTheme.colorScheme.surface
+ )
+ )
+ }
+ }
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
+ if (searchText.text.isBlank() && content != null) {
+ Column(
+ // todo: how and where to best deal with insets?
+ // here? manifest android:windowSoftInputMode="adjustResize"?
+ // other?
+// Modifier
+// .windowInsetsPadding(
+// WindowInsets.safeDrawing.only(
+// WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
+// )
+// )
+ ) {
+ content()
+ }
+ } else {
+ val items = filteredItems(searchText.text)
+ LazyColumn {
+ items(items) {
+ itemContent(it)
}
}
}
@@ -243,8 +233,7 @@ fun ExpandableSearchField(
}) { CloseIcon(android.R.string.cancel) } },
singleLine = true,
colors = colors,
- textStyle = contentTextDirectionStyle,
- keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search)
+ textStyle = contentTextDirectionStyle
)
}
}
diff --git a/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt b/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt
index 19571b21c..acb74220a 100644
--- a/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt
+++ b/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt
@@ -5,41 +5,32 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
+import android.os.Build
import android.os.Bundle
+import android.view.View
+import android.view.WindowInsets.Type
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.activity.ComponentActivity
-import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawing
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
+import androidx.core.view.ViewCompat
import helium314.keyboard.compat.locale
import helium314.keyboard.keyboard.KeyboardSwitcher
import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.InputAttributes
-import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.FileUtils
import helium314.keyboard.latin.define.DebugFlags
import helium314.keyboard.latin.settings.Settings
-import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.ExecutorUtils
+import helium314.keyboard.latin.utils.ResourceUtils
import helium314.keyboard.latin.utils.UncachedInputMethodManagerUtils
import helium314.keyboard.latin.utils.cleanUnusedMainDicts
import helium314.keyboard.latin.utils.prefs
@@ -68,7 +59,6 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
private val crashReportFiles = MutableStateFlow>(emptyList())
private var paused = true
- @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Settings.getValues() == null) {
@@ -76,9 +66,21 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
Settings.getInstance().loadSettings(this, resources.configuration.locale(), inputAttributes)
}
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute { cleanUnusedMainDicts(this) }
- crashReportFiles.value = findCrashReports(!BuildConfig.DEBUG && !DebugFlags.DEBUG_ENABLED)
+ if (BuildConfig.DEBUG || DebugFlags.DEBUG_ENABLED)
+ crashReportFiles.value = findCrashReports()
+ setSystemBarIconColor()
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ // with this the layout edit dialog is not covered by the keyboard
+ // alternative of Modifier.imePadding() and properties = DialogProperties(decorFitsSystemWindows = false) has other weird side effects
+ ViewCompat.setOnApplyWindowInsetsListener(window.decorView.rootView) { _, insets ->
+ @Suppress("DEPRECATION")
+ bottomInsets.value = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
+ insets.getInsets(Type.ime()).bottom
+ else insets.systemWindowInsetBottom
+ insets
+ }
+
settingsContainer = SettingsContainer(this)
val spellchecker = intent?.getBooleanExtra("spellchecker", false) ?: false
@@ -96,47 +98,12 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
|| !UncachedInputMethodManagerUtils.isThisImeEnabled(this, imm)
) }
if (spellchecker)
- Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
- Column(Modifier.padding(innerPadding)) {
- TopAppBar(
- title = { Text(stringResource(R.string.android_spell_checker_settings)) },
- windowInsets = WindowInsets(0),
- navigationIcon = {
- IconButton(onClick = { this@SettingsActivity.finish() }) {
- Icon(
- painterResource(R.drawable.ic_arrow_back),
- stringResource(R.string.spoken_description_action_previous)
- )
- }
- },
- )
- settingsContainer[Settings.PREF_USE_CONTACTS]!!.Preference()
- settingsContainer[Settings.PREF_USE_APPS]!!.Preference()
- settingsContainer[Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE]!!.Preference()
- }
+ Column { // lazy way of implementing spell checker settings
+ settingsContainer[Settings.PREF_USE_CONTACTS]!!.Preference()
+ settingsContainer[Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE]!!.Preference()
}
- else {
+ else
SettingsNavHost(onClickBack = { this.finish() })
- if (showWelcomeWizard) {
- WelcomeWizard(close = { showWelcomeWizard = false }, finish = this::finish)
- } else if (crashReports.isNotEmpty()) {
- ConfirmationDialog(
- cancelButtonText = "ignore",
- onDismissRequest = { crashReportFiles.value = emptyList() },
- neutralButtonText = "delete",
- onNeutral = { crashReports.forEach { it.delete() }; crashReportFiles.value = emptyList() },
- confirmButtonText = "get",
- onConfirmed = {
- val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
- intent.addCategory(Intent.CATEGORY_OPENABLE)
- intent.putExtra(Intent.EXTRA_TITLE, "crash_reports.zip")
- intent.setType("application/zip")
- crashFilePicker.launch(intent)
- },
- content = { Text("Crash report files found") },
- )
- }
- }
if (dictUri != null) {
NewDictionaryDialog(
onDismissRequest = { dictUriFlow.value = null },
@@ -144,6 +111,26 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
mainLocale = null
)
}
+ if (!showWelcomeWizard && !spellchecker && crashReports.isNotEmpty()) {
+ ConfirmationDialog(
+ cancelButtonText = "ignore",
+ onDismissRequest = { crashReportFiles.value = emptyList() },
+ neutralButtonText = "delete",
+ onNeutral = { crashReports.forEach { it.delete() }; crashReportFiles.value = emptyList() },
+ confirmButtonText = "get",
+ onConfirmed = {
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ intent.putExtra(Intent.EXTRA_TITLE, "crash_reports.zip")
+ intent.setType("application/zip")
+ crashFilePicker.launch(intent)
+ },
+ content = { Text("Crash report files found") },
+ )
+ }
+ if (!spellchecker && showWelcomeWizard) {
+ WelcomeWizard(close = { showWelcomeWizard = false }, finish = this::finish)
+ }
}
}
}
@@ -156,8 +143,6 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
}
intent = null
}
-
- enableEdgeToEdge()
}
override fun onStart() {
@@ -190,18 +175,15 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
KeyboardSwitcher.getInstance().setThemeNeedsReload()
}
- private fun findCrashReports(onlyUnprotected: Boolean): List {
- val unprotected = DeviceProtectedUtils.getFilesDir(this)?.listFiles().orEmpty()
- if (onlyUnprotected)
- return unprotected.filter { it.name.startsWith("crash_report") }
-
- val dir = getExternalFilesDir(null)
- val allFiles = dir?.listFiles()?.toList().orEmpty() + unprotected
+ private fun findCrashReports(): List {
+ // find crash report files
+ val dir: File = getExternalFilesDir(null) ?: return emptyList()
+ val allFiles = dir.listFiles() ?: return emptyList()
return allFiles.filter { it.name.startsWith("crash_report") }
}
private fun saveCrashReports(uri: Uri) {
- val files = findCrashReports(false)
+ val files = findCrashReports()
if (files.isEmpty()) return
try {
contentResolver.openOutputStream(uri)?.use {
@@ -224,6 +206,23 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
}
}
+ // deprecated but works... ideally it would be done automatically like it worked before switching to compose
+ private fun setSystemBarIconColor() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return
+ val view = window.decorView
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (ResourceUtils.isNight(resources))
+ view.systemUiVisibility = view.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
+ else
+ view.systemUiVisibility = view.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ } else {
+ if (ResourceUtils.isNight(resources))
+ view.systemUiVisibility = view.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
+ else
+ view.systemUiVisibility = view.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ }
+ }
+
companion object {
// public write so compose previews can show the screens
// having it in a companion object is not ideal as it will stay in memory even after settings are closed
@@ -232,6 +231,9 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
var forceNight: Boolean? = null
var forceTheme: String? = null
+
+ // weird inset forwarding because otherwise layout dialog sometimes doesn't care about keyboard showing
+ var bottomInsets = MutableStateFlow(0)
}
override fun onSharedPreferenceChanged(prefereces: SharedPreferences?, key: String?) {
diff --git a/app/src/main/java/helium314/keyboard/settings/Theme.kt b/app/src/main/java/helium314/keyboard/settings/Theme.kt
index 32d8121b9..e8924a300 100644
--- a/app/src/main/java/helium314/keyboard/settings/Theme.kt
+++ b/app/src/main/java/helium314/keyboard/settings/Theme.kt
@@ -12,6 +12,9 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.text.font.DeviceFontFamilyName
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import helium314.keyboard.latin.R
@@ -33,9 +36,20 @@ fun Theme(dark: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit
MaterialTheme(
colorScheme = colorScheme,
typography = Typography(
- titleLarge = material3.titleLarge.copy(fontWeight = FontWeight.Bold),
- titleMedium = material3.titleMedium.copy(fontWeight = FontWeight.Bold),
- titleSmall = material3.titleSmall.copy(fontWeight = FontWeight.Bold)
+ headlineMedium = material3.headlineMedium.copy(fontWeight = FontWeight.Bold),
+ headlineSmall = material3.headlineSmall.copy(fontWeight = FontWeight.Bold),
+ titleLarge = material3.titleLarge.copy(
+ fontWeight = FontWeight.Bold,
+ fontFamily = FontFamily(Font(DeviceFontFamilyName("sans-serif-condensed"), FontWeight.Bold))
+ ),
+ titleMedium = material3.titleMedium.copy(
+ fontWeight = FontWeight.Bold,
+ fontFamily = FontFamily(Font(DeviceFontFamilyName("sans-serif-condensed"), FontWeight.Bold))
+ ),
+ titleSmall = material3.titleSmall.copy(
+ fontWeight = FontWeight.Bold,
+ fontFamily = FontFamily(Font(DeviceFontFamilyName("sans-serif-condensed"), FontWeight.Bold))
+ )
),
//shapes = Shapes(),
content = content
diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt
index 24101e86e..6c1f84893 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt
@@ -272,8 +272,7 @@ private fun loadColorString(colorString: String, prefs: SharedPreferences): Bool
try {
allColors[ColorType.valueOf(it.key)] = it.value
} catch (_: IllegalArgumentException) {
- if (it.value == 0) // hacky way of storing theme name: put it in a key with value 0
- runCatching { decodeBase36(it.key) }.getOrNull()?.let { themeName = it }
+ themeName = decodeBase36(it.key)
}
}
themeName = KeyboardTheme.getUnusedThemeName(themeName, prefs)
diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
index 516ac5217..7cbc5e942 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
@@ -3,8 +3,11 @@ package helium314.keyboard.settings.dialogs
import android.content.Intent
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -13,6 +16,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -23,7 +28,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -38,11 +45,11 @@ import helium314.keyboard.settings.DeleteButton
import helium314.keyboard.settings.ExpandButton
import helium314.keyboard.settings.Theme
import helium314.keyboard.settings.dictionaryFilePicker
+import helium314.keyboard.settings.preferences.PreferenceCategory
import helium314.keyboard.settings.previewDark
import helium314.keyboard.settings.screens.getUserAndInternalDictionaries
import java.io.File
import java.util.Locale
-import androidx.compose.ui.platform.LocalConfiguration
@Composable
fun DictionaryDialog(
@@ -59,39 +66,24 @@ fun DictionaryDialog(
onConfirmed = {},
confirmButtonText = null,
cancelButtonText = stringResource(R.string.dialog_close),
- title = { Text(locale.localizedDisplayName(ctx.resources)) },
+ title = { Text(locale.localizedDisplayName(ctx)) },
content = {
val state = rememberScrollState()
Column(Modifier.verticalScroll(state)) {
if (hasInternal) {
- val color = if (mainDict == null) MaterialTheme.typography.titleSmall.color
+ val color = if (mainDict == null) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) // for disabled look
- val bottomPadding = if (mainDict == null) 12.dp else 0.dp
- Text(stringResource(R.string.internal_dictionary_summary),
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = bottomPadding),
- color = color,
- style = MaterialTheme.typography.titleSmall
- )
+ Text(stringResource(R.string.internal_dictionary_summary), color = color, modifier = Modifier.fillMaxWidth())
}
if (mainDict != null)
DictionaryDetails(mainDict)
if (addonDicts.isNotEmpty()) {
- HorizontalDivider()
- Text(stringResource(R.string.dictionary_category_title),
- modifier = Modifier.padding(vertical = 12.dp),
- style = MaterialTheme.typography.titleSmall
- )
+ PreferenceCategory(stringResource(R.string.dictionary_settings_title))
addonDicts.forEach { DictionaryDetails(it) }
}
val dictString = createDictionaryTextAnnotated(locale)
if (dictString.isNotEmpty()) {
HorizontalDivider()
- Text(stringResource(R.string.dictionary_available),
- modifier = Modifier.padding(top = 12.dp, bottom = 4.dp),
- style = MaterialTheme.typography.titleSmall
- )
Text(dictString, style = LocalTextStyle.current.merge(lineHeight = 1.8.em))
}
}
@@ -123,12 +115,11 @@ private fun DictionaryDetails(dict: File) {
DeleteButton { showDeleteDialog = true }
ExpandButton { showDetails = !showDetails }
}
- // default animations look better but make the dialog flash, see also MultiSliderPreference
- AnimatedVisibility(showDetails, enter = fadeIn(), exit = fadeOut()) {
+ AnimatedVisibility(showDetails, enter = fadeIn(), exit = fadeOut()) { // default animation looks better, but makes the dialog flash
Text(
- header.info(LocalConfiguration.current.locale()),
+ header.info(LocalContext.current.resources.configuration.locale()),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(start = 10.dp, top = 0.dp, end = 10.dp, bottom = 12.dp)
+ modifier = Modifier.padding(horizontal = 10.dp)
)
}
if (showDeleteDialog)
diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutEditDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutEditDialog.kt
index aae610370..81a3c0451 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutEditDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutEditDialog.kt
@@ -2,15 +2,11 @@
package helium314.keyboard.settings.dialogs
import android.widget.Toast
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.exclude
-import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
@@ -18,11 +14,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.DialogProperties
import helium314.keyboard.keyboard.KeyboardSwitcher
import helium314.keyboard.latin.R
import helium314.keyboard.latin.utils.LayoutType
@@ -55,6 +50,7 @@ fun LayoutEditDialog(
val ctx = LocalContext.current
val scope = rememberCoroutineScope()
val startIsCustom = LayoutUtilsCustom.isCustomLayout(initialLayoutName)
+ val bottomInsets by SettingsActivity.bottomInsets.collectAsState()
var displayNameValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(
if (startIsCustom) LayoutUtilsCustom.getDisplayName(initialLayoutName)
@@ -114,9 +110,10 @@ fun LayoutEditDialog(
}
valid && nameValid // don't allow saving with invalid name, but inform user about issues with layout content
},
- properties = DialogProperties(usePlatformDefaultWidth = false, dismissOnClickOutside = false, dismissOnBackPress = false),
- modifier = Modifier.windowInsetsPadding(WindowInsets.ime.exclude(WindowInsets.systemBars))
- .padding(horizontal = 16.dp), // dialog is rather wide, but shouldn't go all the way to the screen edges
+ // this looks weird when the text field is not covered by the keyboard (long dialog)
+ // but better than not seeing the bottom part of the field...
+ modifier = Modifier.padding(bottom = with(LocalDensity.current)
+ { (bottomInsets / 2 + 36).toDp() }), // why is the /2 necessary?
reducePadding = true,
)
}
diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutPickerDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutPickerDialog.kt
index ace5aafb8..e42631bb7 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutPickerDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutPickerDialog.kt
@@ -204,7 +204,6 @@ private fun LayoutItemRow(
onConfirmed = {
showDeleteDialog = false
onDelete(layoutName)
- (ctx.getActivity() as? SettingsActivity)?.prefChanged()
}
)
}
diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/NewDictionaryDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/NewDictionaryDialog.kt
index a82a07217..2bf3da39c 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/NewDictionaryDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/NewDictionaryDialog.kt
@@ -32,7 +32,6 @@ import helium314.keyboard.settings.DropDownField
import helium314.keyboard.settings.WithSmallTitle
import java.io.File
import java.util.Locale
-import androidx.compose.ui.platform.LocalConfiguration
@Composable
fun NewDictionaryDialog(
@@ -47,16 +46,16 @@ fun NewDictionaryDialog(
} else if (header != null) {
val ctx = LocalContext.current
val dictLocale = header.mLocaleString.constructLocale()
+ var locale by remember { mutableStateOf(mainLocale ?: dictLocale) }
val enabledLanguages = SubtypeSettings.getEnabledSubtypes().map { it.locale().language }
val comparer = compareBy({ it != mainLocale }, { it != dictLocale }, { it.language !in enabledLanguages }, { it.script() != dictLocale.script() })
val locales = SubtypeSettings.getAvailableSubtypeLocales()
.filter { it.script() == dictLocale.script() || it.script() == mainLocale?.script() }
.sortedWith(comparer)
- var locale by remember { mutableStateOf(mainLocale ?: dictLocale.takeIf { it in locales } ?: locales.first()) }
- val cacheDir = DictionaryInfoUtils.getCacheDirectoryForLocale(locale, ctx)
+ val cacheDir = DictionaryInfoUtils.getAndCreateCacheDirectoryForLocale(locale, ctx)
val dictFile = File(cacheDir, header.mIdString.substringBefore(":") + "_" + DictionaryInfoUtils.USER_DICTIONARY_SUFFIX)
val type = header.mIdString.substringBefore(":")
- val info = header.info(LocalConfiguration.current.locale())
+ val info = header.info(ctx.resources.configuration.locale())
ThreeButtonAlertDialog(
onDismissRequest = { onDismissRequest(); cachedFile.delete() },
onConfirmed = {
@@ -65,7 +64,7 @@ fun NewDictionaryDialog(
cachedFile.renameTo(dictFile)
if (type == Dictionary.TYPE_MAIN) {
// replaced main dict, remove the one created from internal data
- val internalMainDictFile = File(cacheDir, DictionaryInfoUtils.MAIN_DICT_FILE_NAME)
+ val internalMainDictFile = File(cacheDir, DictionaryInfoUtils.getExtractedMainDictFilename())
internalMainDictFile.delete()
}
val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)
@@ -81,7 +80,7 @@ fun NewDictionaryDialog(
selectedItem = locale,
onSelected = { locale = it },
items = locales
- ) { Text(it.localizedDisplayName(ctx.resources)) }
+ ) { Text(it.localizedDisplayName(ctx)) }
}
if (locale.script() != dictLocale.script()) {
// whatever, still allow it if the user wants
@@ -93,7 +92,7 @@ fun NewDictionaryDialog(
)
}
if (dictFile.exists()) {
- val oldInfo = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(dictFile, 0, dictFile.length())?.info(LocalConfiguration.current.locale())
+ val oldInfo = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(dictFile, 0, dictFile.length())?.info(ctx.resources.configuration.locale())
HorizontalDivider()
Text(
stringResource(R.string.replace_dictionary_message, type, oldInfo ?: "(no info)", info),
diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/ThreeButtonAlertDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/ThreeButtonAlertDialog.kt
index cc20a4e63..2b34bcd89 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/ThreeButtonAlertDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/ThreeButtonAlertDialog.kt
@@ -64,7 +64,7 @@ fun ThreeButtonAlertDialog(
bottom = if (reducePadding) 2.dp else 6.dp
)) {
title?.let {
- CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleMedium) {
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleLarge) {
Box(Modifier.padding(PaddingValues(bottom = if (reducePadding) 4.dp else 16.dp))) {
title()
}
@@ -82,7 +82,7 @@ fun ThreeButtonAlertDialog(
TextButton(
onClick = onNeutral
) { Text(neutralButtonText) }
- Spacer(Modifier.weight(1f))
+ Spacer(modifier.weight(1f))
TextButton(onClick = onDismissRequest) { Text(cancelButtonText) }
if (confirmButtonText != null)
TextButton(
diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/BackgroundImagePreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/BackgroundImagePreference.kt
index 0f500cdf9..8b70448b8 100644
--- a/app/src/main/java/helium314/keyboard/settings/preferences/BackgroundImagePreference.kt
+++ b/app/src/main/java/helium314/keyboard/settings/preferences/BackgroundImagePreference.kt
@@ -105,7 +105,6 @@ fun BackgroundImagePref(setting: Setting, isLandscape: Boolean) {
getFile().delete()
Settings.clearCachedBackgroundImages()
KeyboardSwitcher.getInstance().setThemeNeedsReload()
- showSelectionDialog = false
}
)
}
diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt
index 159d79671..ee771d5bf 100644
--- a/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt
+++ b/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt
@@ -3,8 +3,6 @@ package helium314.keyboard.settings.preferences
import android.content.Intent
import android.content.SharedPreferences
-import android.os.Looper
-import android.widget.Toast
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -15,12 +13,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import helium314.keyboard.dictionarypack.DictionaryPackConstants
import helium314.keyboard.keyboard.KeyboardSwitcher
-import helium314.keyboard.keyboard.emoji.SupportedEmojis
import helium314.keyboard.latin.R
import helium314.keyboard.latin.checkVersionUpgrade
import helium314.keyboard.latin.common.FileUtils
+import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.DeviceProtectedUtils
+import helium314.keyboard.latin.utils.DictionaryInfoUtils
import helium314.keyboard.latin.utils.ExecutorUtils
import helium314.keyboard.latin.utils.LayoutUtilsCustom
import helium314.keyboard.latin.utils.Log
@@ -52,10 +51,10 @@ fun BackupRestorePreference(setting: Setting) {
val prefs = ctx.prefs()
var error: String? by rememberSaveable { mutableStateOf(null) }
val backupFilePatterns by lazy { listOf(
- "blacklists${File.separator}.*\\.txt".toRegex(),
- "layouts${File.separator}.*${LayoutUtilsCustom.CUSTOM_LAYOUT_PREFIX}+\\..{0,4}".toRegex(), // can't expect a period at the end, as this would break restoring older backups
- "dicts${File.separator}.*${File.separator}.*user\\.dict".toRegex(),
- "UserHistoryDictionary.*${File.separator}UserHistoryDictionary.*\\.(body|header)".toRegex(),
+ "blacklists/.*\\.txt".toRegex(),
+ "layouts/${LayoutUtilsCustom.CUSTOM_LAYOUT_PREFIX}+\\..{0,4}".toRegex(), // can't expect a period at the end, as this would break restoring older backups
+ "dicts/.*/.*user\\.dict".toRegex(),
+ "UserHistoryDictionary.*/UserHistoryDictionary.*\\.(body|header)".toRegex(),
"custom_background_image.*".toRegex(),
"custom_font".toRegex(),
) }
@@ -67,7 +66,7 @@ fun BackupRestorePreference(setting: Setting) {
val files = mutableListOf()
filesDir.walk().forEach { file ->
val path = file.path.replace(filesPath, "")
- if (file.isFile && backupFilePatterns.any { path.matches(it) })
+ if (backupFilePatterns.any { path.matches(it) })
files.add(file)
}
val protectedFilesDir = DeviceProtectedUtils.getFilesDir(ctx)
@@ -75,7 +74,7 @@ fun BackupRestorePreference(setting: Setting) {
val protectedFiles = mutableListOf()
protectedFilesDir.walk().forEach { file ->
val path = file.path.replace(protectedFilesPath, "")
- if (file.isFile && backupFilePatterns.any { path.matches(it) })
+ if (backupFilePatterns.any { path.matches(it) })
protectedFiles.add(file)
}
val wait = CountDownLatch(1)
@@ -132,11 +131,13 @@ fun BackupRestorePreference(setting: Setting) {
if (entry.name.startsWith("unprotected${File.separator}")) {
val adjustedName = entry.name.substringAfter("unprotected${File.separator}")
if (backupFilePatterns.any { adjustedName.matches(it) }) {
- val file = File(deviceProtectedFilesDir, adjustedName)
+ val targetFileName = upgradeFileNames(adjustedName)
+ val file = File(deviceProtectedFilesDir, targetFileName)
FileUtils.copyStreamToNewFile(zip, file)
}
} else if (backupFilePatterns.any { entry!!.name.matches(it) }) {
- val file = File(filesDir, entry.name)
+ val targetFileName = upgradeFileNames(entry.name)
+ val file = File(filesDir, targetFileName)
FileUtils.copyStreamToNewFile(zip, file)
} else if (entry.name == PREFS_FILE_NAME) {
val prefLines = String(zip.readBytes()).split("\n")
@@ -153,9 +154,6 @@ fun BackupRestorePreference(setting: Setting) {
}
}
}
-
- Looper.prepare()
- Toast.makeText(ctx, ctx.getString(R.string.backup_restored), Toast.LENGTH_LONG).show()
} catch (t: Throwable) {
error = "r" + t.message
Log.w("AdvancedScreen", "error during restore", t)
@@ -170,9 +168,7 @@ fun BackupRestorePreference(setting: Setting) {
val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)
ctx.getActivity()?.sendBroadcast(newDictBroadcast)
LayoutUtilsCustom.onLayoutFileChanged()
- LayoutUtilsCustom.removeMissingLayouts(ctx)
(ctx.getActivity() as? SettingsActivity)?.prefChanged()
- SupportedEmojis.load(ctx)
KeyboardSwitcher.getInstance().setThemeNeedsReload()
}
Preference(name = setting.title, onClick = { showDialog = true })
@@ -257,5 +253,38 @@ private fun readJsonLinesToSettings(list: List, prefs: SharedPreferences
}
}
+// todo (later): remove this when new package name has been in use for long enough, this is only for migrating from old openboard name
+private fun upgradeFileNames(originalName: String): String {
+ return when {
+ originalName.endsWith(DictionaryInfoUtils.USER_DICTIONARY_SUFFIX) -> {
+ // replace directory after switch to language tag
+ val dirName = originalName.substringAfter(File.separator).substringBefore(File.separator)
+ originalName.replace(dirName, dirName.constructLocale().toLanguageTag())
+ }
+ originalName.startsWith("blacklists") -> {
+ // replace file name after switch to language tag
+ val fileName = originalName.substringAfter("blacklists${File.separator}").substringBefore(".txt")
+ originalName.replace(fileName, fileName.constructLocale().toLanguageTag())
+ }
+ originalName.startsWith("layouts") -> {
+ // replace file name after switch to language tag, but only if it's not a layout
+ val localeString = originalName.substringAfter(".").substringBefore(".")
+ if (localeString in listOf("symbols", "symbols_shifted", "symbols_arabic", "number", "numpad", "numpad_landscape", "phone", "phone_symbols"))
+ return originalName // it's a layout!
+ val locale = localeString.constructLocale()
+ if (locale.toLanguageTag() != "und")
+ originalName.replace(localeString, locale.toLanguageTag())
+ else
+ originalName // no valid locale -> must be symbols layout, don't change
+ }
+ originalName.startsWith("UserHistoryDictionary") -> {
+ val localeString = originalName.substringAfter(".").substringBefore(".")
+ val locale = localeString.constructLocale()
+ originalName.replace(localeString, locale.toLanguageTag())
+ }
+ else -> originalName
+ }
+}
+
private const val PREFS_FILE_NAME = "preferences.json"
private const val PROTECTED_PREFS_FILE_NAME = "protected_preferences.json"
diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/MultiSliderPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/MultiSliderPreference.kt
deleted file mode 100644
index e991344c9..000000000
--- a/app/src/main/java/helium314/keyboard/settings/preferences/MultiSliderPreference.kt
+++ /dev/null
@@ -1,205 +0,0 @@
-// SPDX-License-Identifier: GPL-3.0-only
-package helium314.keyboard.settings.preferences
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.LocalTextStyle
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Slider
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import helium314.keyboard.latin.R
-import helium314.keyboard.latin.settings.createPrefKeyForBooleanSettings
-import helium314.keyboard.latin.utils.prefs
-import helium314.keyboard.settings.Theme
-import helium314.keyboard.settings.WithSmallTitle
-import helium314.keyboard.settings.dialogs.ThreeButtonAlertDialog
-import helium314.keyboard.settings.previewDark
-
-// too specialized for using a more generic dialog
-// actual key for each setting is baseKey with one _true/_false appended per dimension (need to keep order!)
-// todo: possible adjustments, maybe depending on user feedback
-// should dimension checkboxes have any other effect than just showing / hiding sliders?
-// one could argue that e.g. when disabling the split checkbox, then split mode should not affect the setting
-// store checkbox states?
-// if so, per setting or global?
-// show a description? currently commented because it could get long, even without showing the variations
-// maybe if we store the checkbox state in a setting, we could use it for determining what to show
-@Composable
-fun MultiSliderPreference(
- name: String,
- baseKey: String,
- dimensions: List,
- defaults: Array,
- range: ClosedFloatingPointRange,
- description: (Float) -> String,
- onDone: () -> Unit
-) {
- if (defaults.size != 1.shl(dimensions.size))
- throw ArithmeticException("defaults size does not match with dimensions, expected ${1.shl(dimensions.size)}, got ${defaults.size}")
- var showDialog by remember { mutableStateOf(false) }
- //val (_, keys) = remember { createVariantsAndKeys(dimensions, baseKey) }
- //val prefs = LocalContext.current.prefs()
- Preference(
- name = name,
- onClick = { showDialog = true },
- //description = keys.mapIndexed { i, it -> description(prefs.getFloat(it, defaults[i])) }.joinToString(" $SPLIT ")
- )
- if (showDialog)
- MultiSliderDialog(
- onDismissRequest = { showDialog = false },
- title = { Text(name) },
- baseKey = baseKey,
- onDone = onDone,
- defaultValues = defaults,
- range = range,
- dimensions = dimensions,
- positionString = description
- )
-}
-
-// SliderDialog, but for multiple sliders with same range, each with a different setting and title
-@Composable
-private fun MultiSliderDialog(
- onDismissRequest: () -> Unit,
- title: @Composable () -> Unit,
- baseKey: String,
- onDone: () -> Unit,
- defaultValues: Array,
- range: ClosedFloatingPointRange,
- dimensions: List,
- modifier: Modifier = Modifier,
- positionString: (Float) -> String,
-) {
- val (variants, keys) = createVariantsAndKeys(dimensions, baseKey)
- var checked by remember { mutableStateOf(List(variants.size) { true }) }
- val prefs = LocalContext.current.prefs()
- val done = remember { mutableMapOf Unit>() }
-
- ThreeButtonAlertDialog(
- onDismissRequest = onDismissRequest,
- onConfirmed = { done.values.forEach { it.invoke() }; onDone() },
- modifier = modifier,
- title = title,
- content = {
- CompositionLocalProvider(
- LocalTextStyle provides MaterialTheme.typography.bodyLarge
- ) {
- val state = rememberScrollState()
- Column(Modifier.verticalScroll(state)) {
- if (dimensions.size > 1) {
- dimensions.forEachIndexed { i, dimension ->
- DimensionCheckbox(checked[i], dimension) {
- checked = checked.mapIndexed { j, c -> if (i == j) it else c }
- }
- }
- }
- variants.forEachIndexed { i, variant ->
- val key = keys[i]
- var sliderPosition by remember { mutableFloatStateOf(prefs.getFloat(key, defaultValues[i])) }
- if (!done.contains(variant))
- done[variant] = {
- if (sliderPosition == defaultValues[i])
- prefs.edit().remove(key).apply()
- else
- prefs.edit().putFloat(key, sliderPosition).apply()
- }
- val forbiddenDimensions = dimensions.filterIndexed { index, _ -> !checked[index] }
- val visible = variant.split(SPLIT).none { it in forbiddenDimensions }
- // default animations make the dialog flash (see also DictionaryDialog)
- AnimatedVisibility(visible, exit = fadeOut(), enter = fadeIn()) {
- WithSmallTitle(variant.ifEmpty { stringResource(R.string.button_default) }) {
- Slider(
- value = sliderPosition,
- onValueChange = { sliderPosition = it },
- valueRange = range,
- )
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(positionString(sliderPosition))
- TextButton({ sliderPosition = defaultValues[i] }) { Text(stringResource(R.string.button_default)) }
- }
- Spacer(Modifier.height(6.dp))
- }
- }
- }
- }
- }
- },
- )
-}
-
-@Composable
-private fun DimensionCheckbox(checked: Boolean, dimension: String, onCheckedChange: (Boolean) -> Unit) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth().clickable { onCheckedChange(!checked) }
- ) {
- Checkbox(
- checked = checked,
- onCheckedChange = { onCheckedChange(it) }
- )
- Text(dimension)
- }
-}
-
-private fun createVariantsAndKeys(dimensions: List, baseKey: String): Pair, List> {
- val variants = mutableListOf("")
- val keys = mutableListOf(createPrefKeyForBooleanSettings(baseKey, 0, dimensions.size))
- var i = 1
- dimensions.forEach { dimension ->
- variants.toList().forEach { variant ->
- if (variant.isEmpty()) variants.add(dimension)
- else variants.add(variant + SPLIT + dimension)
- keys.add(createPrefKeyForBooleanSettings(baseKey, i, dimensions.size))
- i++
- }
- }
- return variants to keys
-}
-
-private const val SPLIT = " / "
-
-@Preview
-@Composable
-private fun Preview() {
- Theme(previewDark) {
- MultiSliderDialog(
- onDismissRequest = { },
- onDone = { },
- positionString = { "${it.toInt()}%"},
- defaultValues = Array(8) { 100f - it % 2 * 50f },
- range = 0f..500f,
- title = { Text("bottom padding scale") },
- dimensions = listOf("landscape", "unfolded", "split"),
- baseKey = ""
- )
- }
-}
diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt
index 97138672a..8a52a914d 100644
--- a/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt
+++ b/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt
@@ -27,7 +27,6 @@ fun