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
deleted file mode 100644
index e075b1290..000000000
--- a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.java
+++ /dev/null
@@ -1,1085 +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.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
new file mode 100644
index 000000000..453317397
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.kt
@@ -0,0 +1,821 @@
+/*
+ * 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) {
+ 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 {
+ 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?.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 08096413c..2e98d0064 100644
--- a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorLruCache.java
+++ b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorLruCache.java
@@ -26,6 +26,7 @@ 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) {
@@ -58,10 +59,8 @@ 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, false /* usePersonalizedDicts */,
- false /* forceReloadMainDictionary */, null /* account */,
- mDictionaryNamePrefix, null /* listener */);
+ mDictionaryFacilitator.resetDictionaries(mContext, mLocale, mUseContactsDictionary,
+ mUseAppsDictionary, false, false, mDictionaryNamePrefix, null);
}
}
@@ -77,6 +76,18 @@ 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 29b416a5d..bd86960b3 100644
--- a/app/src/main/java/helium314/keyboard/latin/DictionaryFactory.kt
+++ b/app/src/main/java/helium314/keyboard/latin/DictionaryFactory.kt
@@ -6,88 +6,87 @@
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
-/**
- * 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)
-
- // 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)
-}
-
-/**
- * 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)
+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)
}
- } else {
- readOnlyBinaryDictionary.close()
- killDictionary(file)
+ 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 })
+ }
+
+ 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) {
+ 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)
+ }
+ }
+
+ 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/InputAttributes.java b/app/src/main/java/helium314/keyboard/latin/InputAttributes.java
index a8b51659a..51aae327c 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().hasShortcutIme();
+ || !RichInputMethodManager.getInstance().isShortcutImeReady();
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 8125edc09..9ccedb806 100644
--- a/app/src/main/java/helium314/keyboard/latin/InputView.java
+++ b/app/src/main/java/helium314/keyboard/latin/InputView.java
@@ -19,8 +19,10 @@ 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.PopupSuggestionsView;
+import helium314.keyboard.latin.suggestions.MoreSuggestionsView;
import helium314.keyboard.latin.suggestions.SuggestionStripView;
+import kotlin.Unit;
+
public final class InputView extends FrameLayout {
private final Rect mInputViewRect = new Rect();
@@ -43,10 +45,7 @@ public final class InputView extends FrameLayout {
mMainKeyboardView, suggestionStripView);
mMoreSuggestionsViewCanceler = new MoreSuggestionsViewCanceler(
mMainKeyboardView, suggestionStripView);
- ViewKt.doOnNextLayout(this, v -> {
- Settings.getValues().mColors.setBackground(findViewById(R.id.main_keyboard_frame), ColorType.MAIN_BACKGROUND);
- return null;
- });
+ ViewKt.doOnNextLayout(this, this::onNextLayout);
}
public void setKeyboardTopPadding(final int keyboardTopPadding) {
@@ -104,6 +103,14 @@ 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
.
@@ -223,8 +230,8 @@ public final class InputView extends FrameLayout {
/**
* This class forwards {@link MotionEvent}s happened in the {@link MainKeyboardView} to
- * {@link SuggestionStripView} when the {@link PopupSuggestionsView} is showing.
- * {@link SuggestionStripView} dismisses {@link PopupSuggestionsView} when it receives any event
+ * {@link SuggestionStripView} when the {@link MoreSuggestionsView} is showing.
+ * {@link SuggestionStripView} dismisses {@link MoreSuggestionsView} 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 5fb5a122d..35a689740 100644
--- a/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt
+++ b/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt
@@ -78,7 +78,7 @@ 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 oldScale = Settings.readOneHandedModeScale(context.prefs(), Settings.getValues().mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT)
+ val oldScale = Settings.readOneHandedModeScale(context.prefs(), Settings.getValues().mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE)
val newScale = (oldScale + changePercent / 100f).coerceAtMost(2.5f).coerceAtLeast(0.5f)
if (newScale == oldScale) return@setOnTouchListener true
Settings.getInstance().writeOneHandedModeScale(newScale)
diff --git a/app/src/main/java/helium314/keyboard/latin/LatinIME.java b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
index d2e75a457..2fe9be203 100644
--- a/app/src/main/java/helium314/keyboard/latin/LatinIME.java
+++ b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
@@ -7,6 +7,7 @@
package helium314.keyboard.latin;
import android.annotation.SuppressLint;
+import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -86,6 +87,7 @@ 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;
@@ -100,7 +102,6 @@ import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
/**
@@ -523,6 +524,11 @@ public class LatinIME extends InputMethodService implements
}
final class SubtypeState {
+ // When HintLocales causes a subtype override, we store
+ // the overridden subtype here in order to restore it when
+ // we switch to another input context that has no HintLocales.
+ private InputMethodSubtype mOverriddenByLocale;
+
private InputMethodSubtype mLastActiveSubtype;
private boolean mCurrentSubtypeHasBeenUsed = true; // starting with true avoids immediate switch
@@ -530,6 +536,70 @@ public class LatinIME extends InputMethodService implements
mCurrentSubtypeHasBeenUsed = true;
}
+ // TextFields can provide locale/language hints that the IME should use via 'hintLocales'.
+ // If a matching subtype is found, we temporarily switch to that subtype until
+ // we return to a context that does not provide any hints, or until the user
+ // explicitly changes the language/subtype in use.
+ public InputMethodSubtype getSubtypeForLocales(final RichInputMethodManager richImm, final Iterable locales) {
+ final InputMethodSubtype overriddenByLocale = mOverriddenByLocale;
+ if (locales == null) {
+ if (overriddenByLocale != null) {
+ // no locales provided, so switch back to
+ // whatever subtype was used last time.
+ mOverriddenByLocale = null;
+
+ return overriddenByLocale;
+ }
+
+ return null;
+ }
+
+ final InputMethodSubtype currentSubtype = richImm.getCurrentSubtype().getRawSubtype();
+ final Locale currentSubtypeLocale = richImm.getCurrentSubtypeLocale();
+ final int minimumMatchLevel = 3; // LocaleUtils.LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
+
+ // Try finding a subtype matching the hint language.
+ for (final Locale hintLocale : locales) {
+ if (LocaleUtils.INSTANCE.getMatchLevel(hintLocale, currentSubtypeLocale) >= minimumMatchLevel
+ || CollectionsKt.any(mSettings.getCurrent().mSecondaryLocales,
+ (secLocale) -> LocaleUtils.INSTANCE.getMatchLevel(hintLocale, secLocale) >= minimumMatchLevel)) {
+ // current locales are already a good match, and we want to avoid unnecessary layout switches.
+ return null;
+ }
+
+ final InputMethodSubtype subtypeForHintLocale = richImm.findSubtypeForHintLocale(hintLocale);
+ if (subtypeForHintLocale == null) {
+ continue;
+ }
+
+ if (subtypeForHintLocale.equals(currentSubtype)) {
+ // no need to switch, we already use the correct locale.
+ return null;
+ }
+
+ if (overriddenByLocale == null) {
+ // auto-switching based on hint locale, so store
+ // whatever subtype was in use so we can switch back
+ // to it later when there are no hint locales.
+ mOverriddenByLocale = currentSubtype;
+ }
+
+ return subtypeForHintLocale;
+ }
+
+ return null;
+ }
+
+ public void onSubtypeChanged(final InputMethodSubtype oldSubtype,
+ final InputMethodSubtype newSubtype) {
+ if (oldSubtype != mOverriddenByLocale) {
+ // Whenever the subtype is changed, clear tracking
+ // the subtype that is overridden by a HintLocale as
+ // we no longer have a subtype to automatically switch back to.
+ mOverriddenByLocale = null;
+ }
+ }
+
public void switchSubtype(final RichInputMethodManager richImm) {
final InputMethodSubtype currentSubtype = richImm.getCurrentSubtype().getRawSubtype();
final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype;
@@ -539,7 +609,7 @@ public class LatinIME extends InputMethodService implements
mCurrentSubtypeHasBeenUsed = false;
}
if (currentSubtypeHasBeenUsed
- && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype)
+ && SubtypeSettings.INSTANCE.isEnabled(lastActiveSubtype)
&& !currentSubtype.equals(lastActiveSubtype)) {
switchToSubtype(lastActiveSubtype);
return;
@@ -668,8 +738,8 @@ public class LatinIME extends InputMethodService implements
if (mDictionaryFacilitator.usesSameSettings(
locales,
mSettings.getCurrent().mUseContactsDictionary,
- mSettings.getCurrent().mUsePersonalizedDicts,
- mSettings.getCurrent().mAccount
+ mSettings.getCurrent().mUseAppsDictionary,
+ mSettings.getCurrent().mUsePersonalizedDicts
)) {
return;
}
@@ -685,9 +755,14 @@ 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();
- mDictionaryFacilitator.resetDictionaries(this, locale,
- settingsValues.mUseContactsDictionary, settingsValues.mUsePersonalizedDicts,
- false, settingsValues.mAccount, "", this);
+ 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);
+ }
mInputLogic.mSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold);
}
@@ -696,12 +771,9 @@ public class LatinIME extends InputMethodService implements
*/
/* package private */ void resetSuggestMainDict() {
final SettingsValues settingsValues = mSettings.getCurrent();
- mDictionaryFacilitator.resetDictionaries(this /* context */,
- mDictionaryFacilitator.getMainLocale(), settingsValues.mUseContactsDictionary,
- settingsValues.mUsePersonalizedDicts,
- true /* forceReloadMainDictionary */,
- settingsValues.mAccount, "" /* dictNamePrefix */,
- this /* DictionaryInitializationListener */);
+ mDictionaryFacilitator.resetDictionaries(this, mDictionaryFacilitator.getMainLocale(),
+ settingsValues.mUseContactsDictionary, settingsValues.mUseAppsDictionary,
+ settingsValues.mUsePersonalizedDicts, true, "", this);
}
// used for debug
@@ -813,7 +885,8 @@ public class LatinIME extends InputMethodService implements
mInputView = view;
mInsetsUpdater = ViewOutlineProviderUtilsKt.setInsetsOutlineProvider(view);
updateSoftInputWindowLayoutParameters();
- mSuggestionStripView = view.findViewById(R.id.suggestion_strip_view);
+ mSuggestionStripView = mSettings.getCurrent().mToolbarMode == ToolbarMode.HIDDEN?
+ null : view.findViewById(R.id.suggestion_strip_view);
if (hasSuggestionStripView()) {
mSuggestionStripView.setListener(this, view);
}
@@ -858,13 +931,16 @@ public class LatinIME extends InputMethodService implements
return;
}
InputMethodSubtype oldSubtype = mRichImm.getCurrentSubtype().getRawSubtype();
+
+ mSubtypeState.onSubtypeChanged(oldSubtype, subtype);
StatsUtils.onSubtypeChanged(oldSubtype, subtype);
mRichImm.onSubtypeChanged(subtype);
mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype),
mSettings.getCurrent());
loadKeyboard();
- if (mSuggestionStripView != null)
+ if (hasSuggestionStripView()) {
mSuggestionStripView.setRtl(mRichImm.getCurrentSubtype().isRtlSubtype());
+ }
}
/** alias to onCurrentInputMethodSubtypeChanged with a better name, as it's also used for internal switching */
@@ -876,20 +952,10 @@ public class LatinIME extends InputMethodService implements
super.onStartInput(editorInfo, restarting);
final List hintLocales = EditorInfoCompatUtils.getHintLocales(editorInfo);
- if (hintLocales == null) {
- return;
- }
- // Try switching to a subtype matching the hint language.
- for (final Locale hintLocale : hintLocales) {
- if (LocaleUtils.INSTANCE.getMatchLevel(hintLocale, mRichImm.getCurrentSubtypeLocale()) >= 3
- || CollectionsKt.any(mSettings.getCurrent().mSecondaryLocales, (secLocale) -> LocaleUtils.INSTANCE.getMatchLevel(hintLocale, secLocale) >= 3))
- return; // current locales are already a good match, and we want to avoid unnecessary layout switches
- final InputMethodSubtype newSubtype = mRichImm.findSubtypeForHintLocale(hintLocale);
- if (newSubtype == null) continue;
- if (newSubtype.equals(mRichImm.getCurrentSubtype().getRawSubtype()))
- return; // no need to switch, we already use the correct locale
- mHandler.postSwitchLanguage(newSubtype);
- break;
+ final InputMethodSubtype subtypeForLocales = mSubtypeState.getSubtypeForLocales(mRichImm, hintLocales);
+ if (subtypeForLocales != null) {
+ // found a better subtype using hint locales that we should switch to.
+ mHandler.postSwitchLanguage(subtypeForLocales);
}
}
@@ -962,6 +1028,8 @@ 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
@@ -1157,7 +1225,7 @@ public class LatinIME extends InputMethodService implements
@Override
public void hideWindow() {
- if (mSuggestionStripView != null)
+ if (hasSuggestionStripView() && mSettings.getCurrent().mToolbarMode == ToolbarMode.EXPANDABLE)
mSuggestionStripView.setToolbarVisibility(false);
mKeyboardSwitcher.onHideWindow();
@@ -1213,7 +1281,7 @@ public class LatinIME extends InputMethodService implements
return;
}
final View visibleKeyboardView = mKeyboardSwitcher.getWrapperView();
- if (visibleKeyboardView == null || !hasSuggestionStripView()) {
+ if (visibleKeyboardView == null) {
return;
}
final int inputHeight = mInputView.getHeight();
@@ -1225,8 +1293,13 @@ public class LatinIME extends InputMethodService implements
mInsetsUpdater.setInsets(outInsets);
return;
}
- final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - mSuggestionStripView.getHeight();
- mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY);
+ final int stripHeight = mKeyboardSwitcher.isShowingStripContainer() ? mKeyboardSwitcher.getStripContainer().getHeight() : 0;
+ final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - stripHeight;
+
+ if (hasSuggestionStripView()) {
+ mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY);
+ }
+
// Need to set expanded touchable region only if a keyboard view is being shown.
if (visibleKeyboardView.isShown()) {
final int touchLeft = 0;
@@ -1310,6 +1383,10 @@ 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);
}
@@ -1403,7 +1480,7 @@ public class LatinIME extends InputMethodService implements
// switch IME if wanted and possible
if (switchIme && !switchSubtype && switchInputMethod())
return;
- final boolean hasMoreThanOneSubtype = mRichImm.getMyEnabledInputMethodSubtypeList(false).size() > 1;
+ final boolean hasMoreThanOneSubtype = mRichImm.hasMultipleEnabledSubtypesInThisIme(true);
// switch subtype if wanted, do nothing if no other subtype is available
if (switchSubtype && !switchIme) {
if (hasMoreThanOneSubtype)
@@ -1571,7 +1648,7 @@ public class LatinIME extends InputMethodService implements
dismissGestureFloatingPreviewText /* dismissDelayed */);
}
- public boolean hasSuggestionStripView() {
+ private boolean hasSuggestionStripView() {
return null != mSuggestionStripView;
}
@@ -1960,8 +2037,10 @@ 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
+ case TRIM_MEMORY_RUNNING_LOW, TRIM_MEMORY_RUNNING_CRITICAL, TRIM_MEMORY_COMPLETE -> {
+ KeyboardLayoutSet.onSystemLocaleChanged(); // clears caches, nothing else
+ mKeyboardSwitcher.trimMemory();
+ }
// deallocateMemory always called on hiding, and should not be called when showing
}
}
diff --git a/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java b/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java
deleted file mode 100644
index 607e30a92..000000000
--- a/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java
+++ /dev/null
@@ -1,446 +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.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
new file mode 100644
index 000000000..38ec0931a
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.kt
@@ -0,0 +1,303 @@
+/*
+ * 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 5764bd2d5..0b0ccd8b9 100644
--- a/app/src/main/java/helium314/keyboard/latin/RichInputMethodSubtype.kt
+++ b/app/src/main/java/helium314/keyboard/latin/RichInputMethodSubtype.kt
@@ -10,12 +10,15 @@ 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
/**
@@ -25,7 +28,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 = isRtlLanguage(locale)
+ val isRtlSubtype: Boolean = ScriptUtils.isScriptRtl(locale.script())
fun getExtraValueOf(key: String): String? = rawSubtype.getExtraValueOf(key)
@@ -40,21 +43,9 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
val isCustom: Boolean get() = LayoutUtilsCustom.isCustomLayout(mainLayoutName)
- val fullDisplayName: String get() {
- if (isNoLanguage) {
- return SubtypeLocaleUtils.getMainLayoutDisplayName(rawSubtype)!!
- }
- return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(locale)
- }
+ val fullDisplayName: String get() = SubtypeLocaleUtils.getSubtypeLocaleDisplayName(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)
- }
+ val middleDisplayName: String get() = SubtypeLocaleUtils.getSubtypeLanguageDisplayName(locale)
override fun equals(other: Any?): Boolean {
if (other !is RichInputMethodSubtype) return false
@@ -81,7 +72,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_qwerty)
+ .setSubtypeNameResId(R.string.subtype_no_language)
.setSubtypeIconResId(R.drawable.ic_ime_switcher)
.setSubtypeLocale(SubtypeLocaleUtils.NO_LANGUAGE)
.setSubtypeMode(Constants.Subtype.KEYBOARD_MODE)
@@ -115,11 +106,8 @@ class RichInputMethodSubtype private constructor(val rawSubtype: InputMethodSubt
val noLanguageSubtype: RichInputMethodSubtype get() {
sNoLanguageSubtype?.let { return it }
var noLanguageSubtype = sNoLanguageSubtype
- val rawNoLanguageSubtype = RichInputMethodManager.getInstance()
- .findSubtypeByLocaleAndKeyboardLayoutSet(
- SubtypeLocaleUtils.NO_LANGUAGE.constructLocale(),
- SubtypeLocaleUtils.QWERTY
- )
+ val rawNoLanguageSubtype = SubtypeSettings.getResourceSubtypesForLocale(SubtypeLocaleUtils.NO_LANGUAGE.constructLocale())
+ .firstOrNull { it.mainLayoutNameOrQwerty() == SubtypeLocaleUtils.QWERTY }
if (rawNoLanguageSubtype != null) {
noLanguageSubtype = RichInputMethodSubtype(rawNoLanguageSubtype)
}
@@ -132,4 +120,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
new file mode 100644
index 000000000..e8941c459
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/SingleDictionaryFacilitator.kt
@@ -0,0 +1,121 @@
+// 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.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
+ }
+
+ // ------------ 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/UserBinaryDictionary.java b/app/src/main/java/helium314/keyboard/latin/UserBinaryDictionary.java
index 0ff78fd6e..91d963cf8 100644
--- a/app/src/main/java/helium314/keyboard/latin/UserBinaryDictionary.java
+++ b/app/src/main/java/helium314/keyboard/latin/UserBinaryDictionary.java
@@ -82,8 +82,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
}
public static UserBinaryDictionary getDictionary(
- final Context context, final Locale locale, final File dictFile,
- final String dictNamePrefix, @Nullable final String account) {
+ final Context context, final Locale locale, final File dictFile, final String dictNamePrefix) {
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 d67f50d7e..a5b50f353 100644
--- a/app/src/main/java/helium314/keyboard/latin/WordComposer.java
+++ b/app/src/main/java/helium314/keyboard/latin/WordComposer.java
@@ -218,6 +218,11 @@ public final class WordComposer {
// TODO: compute where that puts us inside the events
}
+ public void resetInvalidCursorPosition() {
+ if (mCursorPositionWithinWord > mCodePointSize)
+ mCursorPositionWithinWord = 0;
+ }
+
public boolean isCursorFrontOrMiddleOfComposingWord() {
if (DebugFlags.DEBUG_ENABLED && mCursorPositionWithinWord > mCodePointSize) {
throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord
@@ -474,6 +479,10 @@ public final class WordComposer {
return mIsBatchMode;
}
+ public void unsetBatchMode() {
+ mIsBatchMode = false;
+ }
+
public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) {
mRejectedBatchModeSuggestion = rejectedSuggestion;
}
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 ffbc13d62..708122828 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/Colors.kt
+++ b/app/src/main/java/helium314/keyboard/latin/common/Colors.kt
@@ -274,11 +274,11 @@ class DynamicColors(context: Context, override val themeStyle: String, override
override fun get(color: ColorType): Int = when (color) {
TOOL_BAR_KEY_ENABLED_BACKGROUND, EMOJI_CATEGORY_SELECTED, ACTION_KEY_BACKGROUND,
CLIPBOARD_PIN, SHIFT_KEY_ICON -> accent
- AUTOFILL_BACKGROUND_CHIP, GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW -> adjustedBackground
+ AUTOFILL_BACKGROUND_CHIP, GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW_BACKGROUND -> adjustedBackground
TOOL_BAR_EXPAND_KEY_BACKGROUND -> if (!isNight) accent else doubleAdjustedBackground
GESTURE_TRAIL -> gesture
- KEY_TEXT, SUGGESTION_AUTO_CORRECT, REMOVE_SUGGESTION_ICON,
- KEY_ICON, ONE_HANDED_MODE_BUTTON, EMOJI_CATEGORY, TOOL_BAR_KEY, FUNCTIONAL_KEY_TEXT -> keyText
+ 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_HINT_TEXT -> keyHintText
SPACE_BAR_TEXT -> spaceBarText
FUNCTIONAL_KEY_BACKGROUND -> functionalKey
@@ -327,7 +327,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override
EMOJI_CATEGORY_SELECTED, CLIPBOARD_PIN, SHIFT_KEY_ICON -> accentColorFilter
REMOVE_SUGGESTION_ICON, EMOJI_CATEGORY, KEY_TEXT,
KEY_ICON, ONE_HANDED_MODE_BUTTON, TOOL_BAR_KEY, TOOL_BAR_EXPAND_KEY -> keyTextFilter
- KEY_PREVIEW -> adjustedBackgroundFilter
+ KEY_PREVIEW_BACKGROUND -> adjustedBackgroundFilter
ACTION_KEY_ICON -> actionKeyIconColorFilter
else -> colorFilter(get(color))
}
@@ -336,7 +336,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override
if (view.background == null)
view.setBackgroundColor(Color.WHITE) // set white to make the color filters work
when (color) {
- KEY_PREVIEW -> view.background.colorFilter = adjustedBackgroundFilter
+ KEY_PREVIEW_BACKGROUND -> view.background.colorFilter = adjustedBackgroundFilter
FUNCTIONAL_KEY_BACKGROUND, KEY_BACKGROUND, MORE_SUGGESTIONS_WORD_BACKGROUND, SPACE_BAR_BACKGROUND, STRIP_BACKGROUND -> setColor(view.background, color)
ONE_HANDED_MODE_BUTTON -> setColor(view.background, if (keyboardBackground == null) MAIN_BACKGROUND else STRIP_BACKGROUND)
MORE_SUGGESTIONS_BACKGROUND -> view.background.colorFilter = backgroundFilter
@@ -472,10 +472,11 @@ class DefaultColors (
TOOL_BAR_KEY_ENABLED_BACKGROUND, EMOJI_CATEGORY_SELECTED, ACTION_KEY_BACKGROUND,
CLIPBOARD_PIN, SHIFT_KEY_ICON -> accent
AUTOFILL_BACKGROUND_CHIP -> if (themeStyle == STYLE_MATERIAL && !hasKeyBorders) background else adjustedBackground
- GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW -> adjustedBackground
+ GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW_BACKGROUND -> adjustedBackground
TOOL_BAR_EXPAND_KEY_BACKGROUND, CLIPBOARD_SUGGESTION_BACKGROUND -> doubleAdjustedBackground
GESTURE_TRAIL -> gesture
- KEY_TEXT, REMOVE_SUGGESTION_ICON, FUNCTIONAL_KEY_TEXT, KEY_ICON -> keyText
+ KEY_TEXT, REMOVE_SUGGESTION_ICON, FUNCTIONAL_KEY_TEXT, KEY_ICON, EMOJI_KEY_TEXT,
+ POPUP_KEY_TEXT, POPUP_KEY_ICON, KEY_PREVIEW_TEXT -> keyText
KEY_HINT_TEXT -> keyHintText
SPACE_BAR_TEXT -> spaceBarText
FUNCTIONAL_KEY_BACKGROUND -> functionalKey
@@ -524,7 +525,7 @@ class DefaultColors (
if (view.background == null)
view.setBackgroundColor(Color.WHITE) // set white to make the color filters work
when (color) {
- KEY_PREVIEW, POPUP_KEYS_BACKGROUND -> view.background.colorFilter = adjustedBackgroundFilter
+ KEY_PREVIEW_BACKGROUND, POPUP_KEYS_BACKGROUND -> view.background.colorFilter = adjustedBackgroundFilter
FUNCTIONAL_KEY_BACKGROUND, KEY_BACKGROUND, MORE_SUGGESTIONS_WORD_BACKGROUND, SPACE_BAR_BACKGROUND, STRIP_BACKGROUND, CLIPBOARD_SUGGESTION_BACKGROUND -> setColor(view.background, color)
ONE_HANDED_MODE_BUTTON -> setColor(view.background, if (keyboardBackground == null) MAIN_BACKGROUND else STRIP_BACKGROUND)
MORE_SUGGESTIONS_BACKGROUND -> view.background.colorFilter = backgroundFilter
@@ -547,7 +548,7 @@ class DefaultColors (
EMOJI_CATEGORY_SELECTED, CLIPBOARD_PIN, SHIFT_KEY_ICON -> accentColorFilter
KEY_TEXT, KEY_ICON -> keyTextFilter
REMOVE_SUGGESTION_ICON, EMOJI_CATEGORY, ONE_HANDED_MODE_BUTTON, TOOL_BAR_KEY, TOOL_BAR_EXPAND_KEY -> suggestionTextFilter
- KEY_PREVIEW -> adjustedBackgroundFilter
+ KEY_PREVIEW_BACKGROUND -> adjustedBackgroundFilter
ACTION_KEY_ICON -> actionKeyIconColorFilter
else -> colorFilter(get(color)) // create color filter (not great for performance, so the frequently used filters should be stored)
}
@@ -620,6 +621,7 @@ enum class ColorType {
CLIPBOARD_PIN,
EMOJI_CATEGORY,
EMOJI_CATEGORY_SELECTED,
+ EMOJI_KEY_TEXT,
FUNCTIONAL_KEY_TEXT,
FUNCTIONAL_KEY_BACKGROUND,
GESTURE_TRAIL,
@@ -628,11 +630,14 @@ enum class ColorType {
KEY_ICON,
KEY_TEXT,
KEY_HINT_TEXT,
- KEY_PREVIEW,
+ KEY_PREVIEW_BACKGROUND,
+ KEY_PREVIEW_TEXT,
MORE_SUGGESTIONS_HINT,
MORE_SUGGESTIONS_BACKGROUND,
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
deleted file mode 100644
index 7e42f380a..000000000
--- a/app/src/main/java/helium314/keyboard/latin/common/ComposedData.java
+++ /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 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
new file mode 100644
index 000000000..091c41920
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/common/ComposedData.kt
@@ -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 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 4dedec0ec..65df85cec 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/Constants.java
+++ b/app/src/main/java/helium314/keyboard/latin/common/Constants.java
@@ -197,6 +197,8 @@ 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/LocaleUtils.kt b/app/src/main/java/helium314/keyboard/latin/common/LocaleUtils.kt
index 377d0c08d..f06ca7332 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,34 +171,32 @@ object LocaleUtils {
}
}
- @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
- }
- }
+ 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)
- 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 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
}
- val localeDisplayName = locale.getDisplayName(displayLocale)
+ if (overrideResId != 0) {
+ return if (displayLocale == null) resources.getString(overrideResId)
+ else runInLocale(resources, displayLocale) { it.getString(overrideResId) }
+ }
+
+ val localeDisplayName = getDisplayName(displayLocale ?: resources.configuration.locale())
return if (localeDisplayName == languageTag) {
- locale.getDisplayName(Locale.US) // try fallback to English name, relevant e.g. fpr pms, see https://github.com/Helium314/HeliBoard/pull/748
+ 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 5b8af87c3..982be6919 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt
@@ -6,67 +6,66 @@ 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.SpacedTokens
import java.math.BigInteger
import java.util.Locale
-fun loopOverCodePoints(s: CharSequence, run: (Int) -> Boolean) {
- val text = if (s is String) s else s.toString()
+fun CharSequence.codePointAt(offset: Int) = Character.codePointAt(this, offset)
+fun CharSequence.codePointBefore(offset: Int) = Character.codePointBefore(this, offset)
+
+inline fun loopOverCodePoints(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
var offset = 0
while (offset < text.length) {
- val codepoint = text.codePointAt(offset)
- if (run(codepoint)) return
- offset += Character.charCount(codepoint)
+ val cp = text.codePointAt(offset)
+ val charCount = Character.charCount(cp)
+ if (loop(cp, charCount)) return
+ offset += charCount
}
}
-fun loopOverCodePointsBackwards(s: CharSequence, run: (Int) -> Boolean) {
- val text = if (s is String) s else s.toString()
+inline fun loopOverCodePointsBackwards(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
var offset = text.length
while (offset > 0) {
- val codepoint = text.codePointBefore(offset)
- if (run(codepoint)) return
- offset -= Character.charCount(codepoint)
+ val cp = text.codePointBefore(offset)
+ val charCount = Character.charCount(cp)
+ if (loop(cp, charCount)) return
+ offset -= charCount
}
}
-fun nonWordCodePointAndNoSpaceBeforeCursor(s: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
+fun nonWordCodePointAndNoSpaceBeforeCursor(text: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
var space = false
var nonWordCodePoint = false
- 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)
+ 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) {
nonWordCodePoint = true
+ }
space && nonWordCodePoint // stop if both are found
}
- return nonWordCodePoint && !space // return true if an non-word codepoint and no space was found
+ return nonWordCodePoint && !space // return true if a non-word codepoint and no space was found
}
-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
+fun hasLetterBeforeLastSpaceBeforeCursor(text: CharSequence): Boolean {
+ loopOverCodePointsBackwards(text) { cp, _ ->
+ if (Character.isWhitespace(cp)) return false
+ else if (Character.isLetter(cp)) return true
+ false // continue
}
- return letter
+ return false
}
-/** 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
+/** 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
while (offset > 0) {
- val codepoint = text.codePointBefore(offset)
+ val codepoint = s.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 && text[offset - 1].code == KeyCode.ZWJ) {
+ if (offset > 0 && s[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
@@ -76,19 +75,17 @@ fun getFullEmojiAtEnd(s: 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 = text.codePointBefore(offset)
+ val codepointBefore = s.codePointBefore(offset)
if (isEmoji(codepointBefore)) {
offset -= Character.charCount(codepointBefore)
continue
}
}
// check the whole text after offset
- val textToCheck = text.substring(offset)
- if (isEmoji(textToCheck)) {
- return textToCheck
- }
+ val textToCheck = s.substring(offset)
+ if (isEmoji(textToCheck)) return textToCheck
}
- return text.substring(offset)
+ return s.substring(offset)
}
/** split the string on the first of consecutive space only, further consecutive spaces are added to the next split */
@@ -110,8 +107,7 @@ fun String.splitOnFirstSpacesOnly(): List {
sb.append(c)
}
}
- if (sb.isNotBlank())
- out.add(sb.toString())
+ if (sb.isNotBlank()) out.add(sb.toString())
return out
}
@@ -120,8 +116,7 @@ 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) }
}
@@ -136,11 +131,9 @@ fun containsValueWhenSplit(string: String?, value: String, split: String): Boole
fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c))
-fun isEmoji(s: CharSequence): Boolean = mightBeEmoji(s) && s.matches(emoRegex)
+fun isEmoji(text: CharSequence): Boolean = mightBeEmoji(text) && text.matches(emoRegex)
-fun String.splitOnWhitespace() = split(whitespaceSplitRegex)
-
-private val whitespaceSplitRegex = "\\s+".toRegex()
+fun String.splitOnWhitespace() = SpacedTokens(this).toList()
// 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 6829f71e3..b8af61f47 100644
--- a/app/src/main/java/helium314/keyboard/latin/define/DebugFlags.kt
+++ b/app/src/main/java/helium314/keyboard/latin/define/DebugFlags.kt
@@ -11,6 +11,7 @@ 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
@@ -27,8 +28,7 @@ object DebugFlags {
fun init(context: Context) {
DEBUG_ENABLED = context.prefs().getBoolean(DebugSettings.PREF_DEBUG_MODE, Defaults.PREF_DEBUG_MODE)
- if (DEBUG_ENABLED || BuildConfig.DEBUG)
- CrashReportExceptionHandler(context.applicationContext).install()
+ CrashReportExceptionHandler(context.applicationContext).install()
}
}
@@ -64,11 +64,17 @@ ${Log.getLog(100).joinToString("\n")}
private fun writeCrashReportToFile(text: String) {
try {
- val dir = appContext.getExternalFilesDir(null) ?: return
+ val dir = appContext.getExternalFilesDir(null)
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(Calendar.getInstance().time)
val crashReportFile = File(dir, "crash_report_$date.txt")
- crashReportFile.writeText(text)
- } catch (ignored: IOException) {
+ 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)
}
}
}
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 83d008642..bb13d49a3 100644
--- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
+++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
@@ -54,6 +54,7 @@ import helium314.keyboard.latin.utils.RecapitalizeStatus;
import helium314.keyboard.latin.utils.ScriptUtils;
import helium314.keyboard.latin.utils.StatsUtils;
import helium314.keyboard.latin.utils.TextRange;
+import helium314.keyboard.latin.utils.TimestampKt;
import java.util.ArrayList;
import java.util.Locale;
@@ -323,7 +324,8 @@ public final class InputLogic {
// Don't allow cancellation of manual pick
mLastComposedWord.deactivate();
// Space state must be updated before calling updateShiftState
- mSpaceState = SpaceState.PHANTOM;
+ if (settingsValues.mAutospaceAfterSuggestion)
+ mSpaceState = SpaceState.PHANTOM;
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
// If we're not showing the "Touch again to save", then update the suggestion strip.
@@ -438,19 +440,30 @@ public final class InputLogic {
mWordBeingCorrectedByCursor = null;
mJustRevertedACommit = false;
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);
+ 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 functional keys (codePoint -1), because otherwise the current word
+ // seems to get deleted / replaced by space during mConnection.endBatchEdit()
+ if (event.getMCodePoint() >= 0x1100 || 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);
+ if (event.getMKeyCode() == KeyCode.DELETE)
+ mWordComposer.resetInvalidCursorPosition();
+ } else {
+ mWordComposer.setHangul(false);
+ final boolean wasComposingWord = mWordComposer.isComposingWord();
+ processedEvent = mWordComposer.processEvent(event);
+ // workaround for space and some other separators deleting / replacing the word
+ if (wasComposingWord && !mWordComposer.isComposingWord()) {
+ mWordComposer.resetInvalidCursorPosition();
+ mConnection.finishComposingText();
+ }
+ }
} else {
mWordComposer.setHangul(false);
processedEvent = mWordComposer.processEvent(event);
@@ -546,7 +559,8 @@ public final class InputLogic {
|| settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
final boolean autoShiftHasBeenOverriden = keyboardSwitcher.getKeyboardShiftMode() !=
getCurrentAutoCapsState(settingsValues);
- mSpaceState = SpaceState.PHANTOM;
+ if (settingsValues.mAutospaceBeforeGestureTyping)
+ mSpaceState = SpaceState.PHANTOM;
if (!autoShiftHasBeenOverriden) {
// When we change the space state, we need to update the shift state of the
// keyboard unless it has been overridden manually. This is happening for example
@@ -686,10 +700,7 @@ public final class InputLogic {
if (mSuggestedWords.isPrediction()) {
inputTransaction.setRequiresUpdateSuggestions();
}
- // undo phantom space if it's because after punctuation
- // users who want to start a sentence with a lowercase letter may not like it
- if (mSpaceState == SpaceState.PHANTOM
- && inputTransaction.getMSettingsValues().isUsuallyFollowedBySpace(mConnection.getCodePointBeforeCursor()))
+ if (mSpaceState == SpaceState.PHANTOM && inputTransaction.getMSettingsValues().mShiftRemovesAutospace)
mSpaceState = SpaceState.NONE;
break;
case KeyCode.SETTINGS:
@@ -756,16 +767,35 @@ public final class InputLogic {
}
break;
case KeyCode.WORD_LEFT:
- sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_CTRL_ON);
+ sendDownUpKeyEventWithMetaState(ScriptUtils.isScriptRtl(currentKeyboardScript)?
+ KeyEvent.KEYCODE_DPAD_RIGHT : KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.META_CTRL_ON);
break;
case KeyCode.WORD_RIGHT:
- sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.META_CTRL_ON);
+ sendDownUpKeyEventWithMetaState(ScriptUtils.isScriptRtl(currentKeyboardScript)?
+ KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.META_CTRL_ON);
break;
case KeyCode.MOVE_START_OF_PAGE:
+ final int selectionEnd = mConnection.getExpectedSelectionEnd();
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.META_CTRL_ON);
+ if (mConnection.getExpectedSelectionStart() > 0 && mConnection.getExpectedSelectionEnd() == selectionEnd) {
+ // unchanged, and we're not at the top -> try a different method (necessary for compose fields)
+ mConnection.setSelection(0, 0);
+ }
break;
case KeyCode.MOVE_END_OF_PAGE:
+ final int selectionStart = mConnection.getExpectedSelectionEnd();
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_END, KeyEvent.META_CTRL_ON);
+ if (mConnection.getExpectedSelectionStart() == selectionStart) {
+ // 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 {
+ mConnection.setSelection(Integer.MAX_VALUE, 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);
+ }
+ }
break;
case KeyCode.UNDO:
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_Z, KeyEvent.META_CTRL_ON);
@@ -776,16 +806,19 @@ public final class InputLogic {
case KeyCode.SPLIT_LAYOUT:
KeyboardSwitcher.getInstance().toggleSplitKeyboardMode();
break;
+ case KeyCode.TIMESTAMP:
+ mLatinIME.onTextInput(TimestampKt.getTimestamp(mLatinIME));
+ 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.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:
+ case KeyCode.CAPS_LOCK, KeyCode.EMOJI, KeyCode.TOGGLE_ONE_HANDED_MODE, KeyCode.SWITCH_ONE_HANDED_MODE:
break;
default:
+ if (KeyCode.INSTANCE.isModifier(event.getMKeyCode()))
+ return; // continuation of previous switch case, but modifiers are in a separate place
if (event.getMMetaState() != 0) {
// need to convert codepoint to KeyEvent.KEYCODE_
final int codeToConvert = event.getMKeyCode() < 0 ? event.getMKeyCode() : event.getMCodePoint();
@@ -930,6 +963,7 @@ public final class InputLogic {
// handleNonSpecialCharacterEvent which has the same name as other handle* methods but is
// not the same.
boolean isComposingWord = mWordComposer.isComposingWord();
+ mWordComposer.unsetBatchMode(); // relevant in case we continue a batch word with normal typing
// if we continue directly after a sometimesWordConnector, restart suggestions for the whole word
// (only with URL detection and suggestions enabled)
@@ -949,7 +983,9 @@ public final class InputLogic {
// TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
// See onStartBatchInput() to see how to do it.
if (SpaceState.PHANTOM == inputTransaction.getMSpaceState()
- && !settingsValues.isWordConnector(codePoint)) {
+ && !settingsValues.isWordConnector(codePoint)
+ && !settingsValues.isUsuallyFollowedBySpace(codePoint) // only relevant in rare cases
+ ) {
if (isComposingWord) {
// Sanity check
throw new RuntimeException("Should not be composing here");
@@ -1127,7 +1163,7 @@ public final class InputLogic {
// A double quote behaves like it's usually followed by space if we're inside
// a double quote.
if (wasComposingWord
- && settingsValues.mAutospaceAfterPunctuationEnabled
+ && settingsValues.mAutospaceAfterPunctuation
&& (settingsValues.isUsuallyFollowedBySpace(codePoint) || isInsideDoubleQuoteOrAfterDigit)) {
mSpaceState = SpaceState.PHANTOM;
}
@@ -1196,7 +1232,7 @@ public final class InputLogic {
}
inputTransaction.setRequiresUpdateSuggestions();
} else {
- if (mLastComposedWord.canRevertCommit()) {
+ if (mLastComposedWord.canRevertCommit() && inputTransaction.getMSettingsValues().mBackspaceRevertsAutocorrect) {
final String lastComposedWord = mLastComposedWord.mTypedWord;
revertCommit(inputTransaction);
StatsUtils.onRevertAutoCorrect();
@@ -2168,6 +2204,7 @@ public final class InputLogic {
&& !(mConnection.getCodePointBeforeCursor() == Constants.CODE_PERIOD && mConnection.wordBeforeCursorMayBeEmail())
) {
mConnection.commitCodePoint(Constants.CODE_SPACE);
+ // todo: why not remove phantom space state?
}
}
@@ -2202,12 +2239,14 @@ public final class InputLogic {
mConnection.beginBatchEdit();
if (SpaceState.PHANTOM == mSpaceState) {
insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues);
+ mSpaceState = SpaceState.NONE;
}
mWordComposer.setBatchInputWord(batchInputText);
setComposingTextInternal(batchInputText, 1);
mConnection.endBatchEdit();
// Space state must be updated before calling updateShiftState
- mSpaceState = SpaceState.PHANTOM;
+ if (settingsValues.mAutospaceAfterGestureTyping)
+ mSpaceState = SpaceState.PHANTOM;
keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState());
}
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 93f7f76cb..dc8692c6a 100644
--- a/app/src/main/java/helium314/keyboard/latin/personalization/PersonalizationHelper.java
+++ b/app/src/main/java/helium314/keyboard/latin/personalization/PersonalizationHelper.java
@@ -10,7 +10,6 @@ import android.content.Context;
import helium314.keyboard.latin.utils.Log;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import helium314.keyboard.latin.common.FileUtils;
@@ -31,12 +30,8 @@ public class PersonalizationHelper {
sLangUserHistoryDictCache = new ConcurrentHashMap<>();
@NonNull
- public static UserHistoryDictionary getUserHistoryDictionary(
- final Context context, final Locale locale, @Nullable final String accountName) {
+ public static UserHistoryDictionary getUserHistoryDictionary(final Context context, final Locale locale) {
String lookupStr = locale.toString();
- if (accountName != null) {
- lookupStr += "." + accountName;
- }
synchronized (sLangUserHistoryDictCache) {
if (sLangUserHistoryDictCache.containsKey(lookupStr)) {
final SoftReference ref =
@@ -50,8 +45,7 @@ public class PersonalizationHelper {
return dict;
}
}
- final UserHistoryDictionary dict = new UserHistoryDictionary(
- context, locale, accountName);
+ final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale);
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 b860b1c96..98645080e 100644
--- a/app/src/main/java/helium314/keyboard/latin/personalization/UserHistoryDictionary.java
+++ b/app/src/main/java/helium314/keyboard/latin/personalization/UserHistoryDictionary.java
@@ -30,9 +30,8 @@ public class UserHistoryDictionary extends ExpandableBinaryDictionary {
static final String NAME = UserHistoryDictionary.class.getSimpleName();
// TODO: Make this constructor private
- 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);
+ UserHistoryDictionary(final Context context, final Locale locale) {
+ super(context, getUserHistoryDictName(NAME, locale, null), locale, Dictionary.TYPE_USER_HISTORY, null);
if (mLocale != null && mLocale.toString().length() > 1) {
reloadDictionaryIfRequired();
}
@@ -41,14 +40,13 @@ 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, @Nullable final String account) {
+ static String getUserHistoryDictName(final String name, final Locale locale, @Nullable final File dictFile) {
return getDictName(name, locale, dictFile);
}
public static UserHistoryDictionary getDictionary(final Context context, final Locale locale,
- final File dictFile, final String dictNamePrefix, @Nullable final String account) {
- return PersonalizationHelper.getUserHistoryDictionary(context, locale, account);
+ final File dictFile, final String dictNamePrefix) {
+ return PersonalizationHelper.getUserHistoryDictionary(context, locale);
}
/**
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 0301409ba..f8a91e0ea 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
@@ -9,7 +9,6 @@ import helium314.keyboard.keyboard.KeyboardTheme
import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.common.Constants.Separators
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue
-import helium314.keyboard.latin.utils.JniUtils
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.POPUP_KEYS_LABEL_DEFAULT
import helium314.keyboard.latin.utils.POPUP_KEYS_ORDER_DEFAULT
@@ -64,9 +63,11 @@ object Defaults {
const val PREF_MORE_AUTO_CORRECTION = false
const val PREF_AUTO_CORRECT_THRESHOLD = 0.185f
const val PREF_AUTOCORRECT_SHORTCUTS = true
+ const val PREF_BACKSPACE_REVERTS_AUTOCORRECT = true
const val PREF_CENTER_SUGGESTION_TEXT_TO_ENTER = false
const val PREF_SHOW_SUGGESTIONS = true
const val PREF_ALWAYS_SHOW_SUGGESTIONS = false
+ const val PREF_ALWAYS_SHOW_SUGGESTIONS_EXCEPT_WEB_TEXT = true
const val PREF_KEY_USE_PERSONALIZED_DICTS = true
const val PREF_KEY_USE_DOUBLE_SPACE_PERIOD = true
const val PREF_BLOCK_POTENTIALLY_OFFENSIVE = true
@@ -74,9 +75,9 @@ object Defaults {
const val PREF_LANGUAGE_SWITCH_KEY = "internal"
const val PREF_SHOW_EMOJI_KEY = false
const val PREF_VARIABLE_TOOLBAR_DIRECTION = true
- const val PREF_ADDITIONAL_SUBTYPES = "de${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=qwerty${Separators.SETS}" +
- "fr${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=qwertz${Separators.SETS}" +
- "hu${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=qwerty"
+ const val PREF_ADDITIONAL_SUBTYPES = "de${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwerty${Separators.SETS}" +
+ "fr${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwertz${Separators.SETS}" +
+ "hu${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwerty"
const val PREF_ENABLE_SPLIT_KEYBOARD = false
const val PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE = false
const val PREF_SPLIT_SPACER_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
@@ -88,10 +89,16 @@ object Defaults {
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_EMOJI_KEY_FIT = true
+ const val PREF_EMOJI_SKIN_TONE = ""
const val PREF_SPACE_HORIZONTAL_SWIPE = "move_cursor"
const val PREF_SPACE_VERTICAL_SWIPE = "none"
const val PREF_DELETE_SWIPE = true
const val PREF_AUTOSPACE_AFTER_PUNCTUATION = false
+ const val PREF_AUTOSPACE_AFTER_SUGGESTION = true
+ const val PREF_AUTOSPACE_AFTER_GESTURE_TYPING = true
+ const val PREF_AUTOSPACE_BEFORE_GESTURE_TYPING = true
+ const val PREF_SHIFT_REMOVES_AUTOSPACE = false
const val PREF_ALWAYS_INCOGNITO_MODE = false
const val PREF_BIGRAM_PREDICTIONS = true
const val PREF_SUGGEST_CLIPBOARD_CONTENT = true
@@ -110,6 +117,7 @@ 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")
@@ -123,6 +131,7 @@ object Defaults {
const val PREF_POPUP_KEYS_ORDER = POPUP_KEYS_ORDER_DEFAULT
const val PREF_POPUP_KEYS_LABELS_ORDER = POPUP_KEYS_LABEL_DEFAULT
const val PREF_SHOW_POPUP_HINTS = false
+ const val PREF_SHOW_TLD_POPUP_KEYS = true
const val PREF_MORE_POPUP_KEYS = "main"
const val PREF_SPACE_TO_CHANGE_LANG = true
const val PREF_LANGUAGE_SWIPE_DISTANCE = 5
@@ -136,6 +145,8 @@ 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
@@ -145,15 +156,14 @@ object Defaults {
const val PREF_ABC_AFTER_EMOJI = false
const val PREF_ABC_AFTER_CLIP = false
const val PREF_ABC_AFTER_SYMBOL_SPACE = true
+ const val PREF_ABC_AFTER_NUMPAD_SPACE = false
const val PREF_REMOVE_REDUNDANT_POPUPS = false
const val PREF_SPACE_BAR_TEXT = ""
- @JvmField
+ const val PREF_TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss"
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
const val PREF_PINNED_CLIPS = ""
- @JvmField
- val PREF_LIBRARY_CHECKSUM: String = JniUtils.expectedDefaultChecksum()
const val PREF_SHOW_DEBUG_SETTINGS = false
val PREF_DEBUG_MODE = BuildConfig.DEBUG
const val PREF_SHOW_SUGGESTION_INFOS = false
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 89e93e8dd..87449c563 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
@@ -37,6 +37,7 @@ 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;
@@ -71,9 +72,11 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_MORE_AUTO_CORRECTION = "more_auto_correction";
public static final String PREF_AUTO_CORRECT_THRESHOLD = "auto_correct_threshold";
public static final String PREF_AUTOCORRECT_SHORTCUTS = "autocorrect_shortcuts";
+ public static final String PREF_BACKSPACE_REVERTS_AUTOCORRECT = "backspace_reverts_autocorrect";
public static final String PREF_CENTER_SUGGESTION_TEXT_TO_ENTER = "center_suggestion_text_to_enter";
public static final String PREF_SHOW_SUGGESTIONS = "show_suggestions";
public static final String PREF_ALWAYS_SHOW_SUGGESTIONS = "always_show_suggestions";
+ public static final String PREF_ALWAYS_SHOW_SUGGESTIONS_EXCEPT_WEB_TEXT = "always_show_suggestions_except_web_text";
public static final String PREF_KEY_USE_PERSONALIZED_DICTS = "use_personalized_dicts";
public static final String PREF_KEY_USE_DOUBLE_SPACE_PERIOD = "use_double_space_period";
public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE = "block_potentially_offensive";
@@ -93,10 +96,16 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
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_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";
public static final String PREF_SUGGEST_CLIPBOARD_CONTENT = "suggest_clipboard_content";
@@ -114,6 +123,7 @@ 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";
// one-handed mode gravity, enablement and scale, stored separately per orientation
@@ -131,6 +141,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_POPUP_KEYS_LABELS_ORDER = "popup_keys_labels_order";
public static final String PREF_SHOW_POPUP_HINTS = "show_popup_hints";
public static final String PREF_MORE_POPUP_KEYS = "more_popup_keys";
+ public static final String PREF_SHOW_TLD_POPUP_KEYS = "show_tld_popup_keys";
public static final String PREF_SPACE_TO_CHANGE_LANG = "prefs_long_press_keyboard_to_change_lang";
public static final String PREF_LANGUAGE_SWIPE_DISTANCE = "language_swipe_distance";
@@ -154,8 +165,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_ABC_AFTER_EMOJI = "abc_after_emoji";
public static final String PREF_ABC_AFTER_CLIP = "abc_after_clip";
public static final String PREF_ABC_AFTER_SYMBOL_SPACE = "abc_after_symbol_space";
+ public static final String PREF_ABC_AFTER_NUMPAD_SPACE = "abc_after_numpad_space";
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";
@@ -312,8 +327,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
mPrefs.edit().putBoolean(Settings.PREF_ALWAYS_INCOGNITO_MODE, !oldValue).apply();
}
- public static void writePrefAdditionalSubtypes(final SharedPreferences prefs, final String prefSubtypes) {
- prefs.edit().putString(PREF_ADDITIONAL_SUBTYPES, prefSubtypes).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) {
@@ -356,7 +371,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public void writeOneHandedModeEnabled(final boolean enabled) {
mPrefs.edit().putBoolean(PREF_ONE_HANDED_MODE_PREFIX +
- (mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), enabled).apply();
+ (mSettingsValues.mDisplayOrientation != Configuration.ORIENTATION_LANDSCAPE), enabled).apply();
}
public static float readOneHandedModeScale(final SharedPreferences prefs, final boolean isLandscape) {
@@ -365,7 +380,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public void writeOneHandedModeScale(final Float scale) {
mPrefs.edit().putFloat(PREF_ONE_HANDED_SCALE_PREFIX +
- (mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), scale).apply();
+ (mSettingsValues.mDisplayOrientation != Configuration.ORIENTATION_LANDSCAPE), scale).apply();
}
public static int readOneHandedModeGravity(final SharedPreferences prefs, final boolean isLandscape) {
@@ -374,7 +389,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public void writeOneHandedModeGravity(final int gravity) {
mPrefs.edit().putInt(PREF_ONE_HANDED_GRAVITY_PREFIX +
- (mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), gravity).apply();
+ (mSettingsValues.mDisplayOrientation != Configuration.ORIENTATION_LANDSCAPE), gravity).apply();
}
public void writeSplitKeyboardEnabled(final boolean enabled, final boolean isLandscape) {
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsSubtype.kt b/app/src/main/java/helium314/keyboard/latin/settings/SettingsSubtype.kt
index a5745295b..b966d92d8 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsSubtype.kt
+++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsSubtype.kt
@@ -11,39 +11,25 @@ import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.define.DebugFlags
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.LayoutType.Companion.toExtraValue
-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.SubtypeUtilsAdditional
import helium314.keyboard.latin.utils.locale
import java.util.Locale
// some kind of intermediate between the string stored in preferences and an InputMethodSubtype
+// todo: consider using a hashMap or sortedMap instead of a string if we run into comparison issues once again
data class SettingsSubtype(val locale: Locale, val extraValues: String) {
fun toPref() = locale.toLanguageTag() + Separators.SET + extraValues
/** Creates an additional subtype from the SettingsSubtype.
* Resulting InputMethodSubtypes are equal if SettingsSubtypes are equal */
- fun toAdditionalSubtype(): InputMethodSubtype? {
+ fun toAdditionalSubtype(): InputMethodSubtype {
val asciiCapable = locale.script() == ScriptUtils.SCRIPT_LATIN
- val subtype = SubtypeUtilsAdditional.createAdditionalSubtype(locale, extraValues, asciiCapable, true)
-
- // todo: this is returns null for all non-latin layouts
- // either fix it, or remove the check
- // if removed, removing a layout will result in fallback qwerty even for non-ascii, but this is better than the current alternative
-/* if (subtype.nameResId == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT
- && mainLayoutName()?.endsWith("+") != true // "+" layouts and custom layouts are always "unknown"
- && !LayoutUtilsCustom.isCustomLayout(mainLayoutName() ?: SubtypeLocaleUtils.QWERTY)
- ) {
- // Skip unknown keyboard layout subtype. This may happen when predefined keyboard
- // layout has been removed.
- Log.w(SettingsSubtype::class.simpleName, "unknown additional subtype $this")
- return null
- }*/
- return subtype
+ return SubtypeUtilsAdditional.createAdditionalSubtype(locale, extraValues, asciiCapable, true)
}
fun mainLayoutName() = LayoutType.getMainLayoutFromExtraValue(extraValues)
@@ -54,7 +40,7 @@ data class SettingsSubtype(val locale: Locale, val extraValues: String) {
val newList = extraValues.split(",")
.filterNot { it.isBlank() || it.startsWith("$extraValueKey=") || it == extraValueKey }
val newValue = if (extraValue == null) extraValueKey else "$extraValueKey=$extraValue"
- val newValues = (newList + newValue).joinToString(",")
+ val newValues = (newList + newValue).sorted().joinToString(",")
return copy(extraValues = newValues)
}
@@ -86,6 +72,8 @@ data class SettingsSubtype(val locale: Locale, val extraValues: String) {
prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
.split(Separators.SETS).contains(toPref())
+ fun isSameAsDefault() = SubtypeSettings.getResourceSubtypesForLocale(locale).any { it.toSettingsSubtype() == this.toPref().toSettingsSubtype() }
+
companion object {
fun String.toSettingsSubtype(): SettingsSubtype =
SettingsSubtype(
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 8576bc1d2..26ae4658c 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
+++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
@@ -11,11 +11,11 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.text.InputType;
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,6 +31,7 @@ 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;
@@ -65,6 +66,7 @@ public class SettingsValues {
public final boolean mShowNumberRowHints;
public final boolean mShowsHints;
public final boolean mShowsPopupHints;
+ public final boolean mShowTldPopupKeys;
public final boolean mSpaceForLangChange;
public final boolean mShowsEmojiKey;
public final boolean mVarToolbarDirection;
@@ -75,7 +77,11 @@ public class SettingsValues {
public final int mSpaceSwipeVertical;
public final int mLanguageSwipeDistance;
public final boolean mDeleteSwipeEnabled;
- public final boolean mAutospaceAfterPunctuationEnabled;
+ public final boolean mAutospaceAfterPunctuation;
+ public final boolean mAutospaceAfterSuggestion;
+ public final boolean mAutospaceAfterGestureTyping;
+ public final boolean mAutospaceBeforeGestureTyping;
+ public final boolean mShiftRemovesAutospace;
public final boolean mClipboardHistoryEnabled;
public final long mClipboardHistoryRetentionTime;
public final boolean mOneHandedModeEnabled;
@@ -103,31 +109,39 @@ 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;
public final boolean mAlphaAfterClipHistoryEntry;
public final boolean mAlphaAfterSymbolAndSpace;
+ public final boolean mAlphaAfterNumpadAndSpace;
public final boolean mRemoveRedundantPopups;
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;
public final boolean mAutoCorrectEnabled;
public final float mAutoCorrectionThreshold;
+ public final boolean mBackspaceRevertsAutocorrect;
public final int mScoreLimitForAutocorrect;
public final boolean mAutoCorrectShortcuts;
private final boolean mSuggestionsEnabledPerUserSettings;
@@ -136,14 +150,10 @@ 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) {
@@ -155,6 +165,8 @@ 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);
@@ -172,9 +184,10 @@ public class SettingsValues {
mShowNumberRowHints = prefs.getBoolean(Settings.PREF_SHOW_NUMBER_ROW_HINTS, Defaults.PREF_SHOW_NUMBER_ROW_HINTS);
mShowsHints = prefs.getBoolean(Settings.PREF_SHOW_HINTS, Defaults.PREF_SHOW_HINTS);
mShowsPopupHints = prefs.getBoolean(Settings.PREF_SHOW_POPUP_HINTS, Defaults.PREF_SHOW_POPUP_HINTS);
+ 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 = prefs.getBoolean(Settings.PREF_VARIABLE_TOOLBAR_DIRECTION, Defaults.PREF_VARIABLE_TOOLBAR_DIRECTION);
+ mVarToolbarDirection = mToolbarMode != ToolbarMode.HIDDEN && 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;
@@ -191,6 +204,7 @@ public class SettingsValues {
mScoreLimitForAutocorrect = (mAutoCorrectionThreshold < 0) ? 600000 // very aggressive
: (mAutoCorrectionThreshold < 0.07 ? 800000 : 950000); // aggressive or modest
mAutoCorrectShortcuts = prefs.getBoolean(Settings.PREF_AUTOCORRECT_SHORTCUTS, Defaults.PREF_AUTOCORRECT_SHORTCUTS);
+ mBackspaceRevertsAutocorrect = prefs.getBoolean(Settings.PREF_BACKSPACE_REVERTS_AUTOCORRECT, Defaults.PREF_BACKSPACE_REVERTS_AUTOCORRECT);
mBigramPredictionEnabled = prefs.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, Defaults.PREF_BIGRAM_PREDICTIONS);
mSuggestClipboardContent = prefs.getBoolean(Settings.PREF_SUGGEST_CLIPBOARD_CONTENT, Defaults.PREF_SUGGEST_CLIPBOARD_CONTENT);
mDoubleSpacePeriodTimeout = 1100; // ms
@@ -202,7 +216,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 = prefs.getBoolean(Settings.PREF_QUICK_PIN_TOOLBAR_KEYS, Defaults.PREF_QUICK_PIN_TOOLBAR_KEYS);
+ mQuickPinToolbarKeys = mToolbarMode == ToolbarMode.EXPANDABLE && prefs.getBoolean(Settings.PREF_QUICK_PIN_TOOLBAR_KEYS, Defaults.PREF_QUICK_PIN_TOOLBAR_KEYS);
mScreenMetrics = Settings.readScreenMetrics(res);
// Compute other readable settings
@@ -217,11 +231,15 @@ 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);
- mAccount = null; // remove? or can it be useful somewhere?
- mOverrideShowingSuggestions = mInputAttributes.mMayOverrideShowingSuggestions && prefs.getBoolean(Settings.PREF_ALWAYS_SHOW_SUGGESTIONS, Defaults.PREF_ALWAYS_SHOW_SUGGESTIONS);
+ mSuggestionStripHiddenPerUserSettings = mToolbarMode == ToolbarMode.HIDDEN || mToolbarMode == ToolbarMode.TOOLBAR_KEYS;
+ 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;
+ mSuggestionsEnabledPerUserSettings = ((mInputAttributes.mShouldShowSuggestions && suggestionsEnabled)
+ || mOverrideShowingSuggestions) && !mSuggestionStripHiddenPerUserSettings;
+ mSecondaryStripVisible = mToolbarMode != ToolbarMode.HIDDEN || ! mToolbarHidingGlobal;
mIncognitoModeEnabled = prefs.getBoolean(Settings.PREF_ALWAYS_INCOGNITO_MODE, Defaults.PREF_ALWAYS_INCOGNITO_MODE) || mInputAttributes.mNoLearning
|| mInputAttributes.mIsPasswordField;
mKeyboardHeightScale = prefs.getFloat(Settings.PREF_KEYBOARD_HEIGHT_SCALE, Defaults.PREF_KEYBOARD_HEIGHT_SCALE);
@@ -229,7 +247,11 @@ public class SettingsValues {
mSpaceSwipeVertical = Settings.readVerticalSpaceSwipe(prefs);
mLanguageSwipeDistance = prefs.getInt(Settings.PREF_LANGUAGE_SWIPE_DISTANCE, Defaults.PREF_LANGUAGE_SWIPE_DISTANCE);
mDeleteSwipeEnabled = prefs.getBoolean(Settings.PREF_DELETE_SWIPE, Defaults.PREF_DELETE_SWIPE);
- mAutospaceAfterPunctuationEnabled = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, Defaults.PREF_AUTOSPACE_AFTER_PUNCTUATION);
+ mAutospaceAfterPunctuation = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, Defaults.PREF_AUTOSPACE_AFTER_PUNCTUATION);
+ mAutospaceAfterSuggestion = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_SUGGESTION, Defaults.PREF_AUTOSPACE_AFTER_SUGGESTION);
+ mAutospaceAfterGestureTyping = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_GESTURE_TYPING, Defaults.PREF_AUTOSPACE_AFTER_GESTURE_TYPING);
+ mAutospaceBeforeGestureTyping = prefs.getBoolean(Settings.PREF_AUTOSPACE_BEFORE_GESTURE_TYPING, Defaults.PREF_AUTOSPACE_BEFORE_GESTURE_TYPING);
+ mShiftRemovesAutospace = prefs.getBoolean(Settings.PREF_SHIFT_REMOVES_AUTOSPACE, Defaults.PREF_SHIFT_REMOVES_AUTOSPACE);
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);
@@ -251,6 +273,7 @@ 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(
@@ -261,16 +284,17 @@ public class SettingsValues {
mBottomPaddingScale = Settings.readBottomPaddingScale(prefs, isLandscape);
mSidePaddingScale = Settings.readSidePaddingScale(prefs, isLandscape);
mLongPressSymbolsForNumpad = prefs.getBoolean(Settings.PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD, Defaults.PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD);
- 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);
+ 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);
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() {
@@ -334,11 +358,12 @@ public class SettingsValues {
return mDisplayOrientation == configuration.orientation;
}
- private static boolean readUseContactsEnabled(final SharedPreferences prefs, final Context context) {
+ private static boolean readUseContactsEnabled(final SharedPreferences prefs, final Context ctx) {
final boolean setting = prefs.getBoolean(Settings.PREF_USE_CONTACTS, Defaults.PREF_USE_CONTACTS);
if (!setting) return false;
- if (PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS))
+ if (PermissionsUtil.checkAllPermissionsGranted(ctx, 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 807561799..73d66ad30 100644
--- a/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java
@@ -83,6 +83,7 @@ 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);
}
@@ -93,13 +94,19 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
@Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
- if (Settings.PREF_USE_CONTACTS.equals(key)) {
+ if (key != null) switch (key) {
+ case Settings.PREF_USE_CONTACTS -> {
final boolean useContactsDictionary = prefs.getBoolean(Settings.PREF_USE_CONTACTS, Defaults.PREF_USE_CONTACTS);
mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary);
- } else if (Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE.equals(key)) {
+ }
+ 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 -> {
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/suggestions/MoreSuggestions.java b/app/src/main/java/helium314/keyboard/latin/suggestions/MoreSuggestions.java
index 2799c1f7f..0d52736f5 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 PopupSuggestionsView mPaneView;
+ private final MoreSuggestionsView mPaneView;
private SuggestedWords mSuggestedWords;
private int mFromIndex;
private int mToIndex;
- public Builder(final Context context, final PopupSuggestionsView paneView) {
+ public Builder(final Context context, final MoreSuggestionsView 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
new file mode 100644
index 000000000..40c354291
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/suggestions/MoreSuggestionsView.kt
@@ -0,0 +1,209 @@
+/*
+ * 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 07535c641..e7cf2ed56 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, x);
+ layoutDebugInfo(positionInStrip, placerView, (int) stripView.getX() + x);
}
}
return startIndexOfMoreSuggestions;
@@ -436,8 +436,7 @@ final class SuggestionStripLayoutHelper {
placerView.addView(debugInfoView);
debugInfoView.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int infoWidth = debugInfoView.getMeasuredWidth();
- final int y = debugInfoView.getMeasuredHeight();
- ViewLayoutUtils.placeViewAt(debugInfoView, x - infoWidth, y, infoWidth, debugInfoView.getMeasuredHeight());
+ ViewLayoutUtils.placeViewAt(debugInfoView, x - infoWidth, 0, 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
deleted file mode 100644
index 860759831..000000000
--- a/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.java
+++ /dev/null
@@ -1,735 +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 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
new file mode 100644
index 000000000..8f3148e60
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt
@@ -0,0 +1,528 @@
+/*
+ * 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
+
+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()
+ init {
+ val colors = Settings.getValues().mColors
+
+ // expand key
+ 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 direction = 1 // 1 if LTR, -1 if RTL
+ 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
+ }
+
+ 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/DialogUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/DialogUtils.kt
index 470743bbb..e13f2f101 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/DialogUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/DialogUtils.kt
@@ -1,7 +1,7 @@
package helium314.keyboard.latin.utils
import android.content.Context
-import androidx.appcompat.view.ContextThemeWrapper
+import android.view.ContextThemeWrapper
import helium314.keyboard.latin.R
// todo: ideally the custom InputMethodPicker would be removed / replaced with compose dialog, then this can be removed
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.java
deleted file mode 100644
index b27de7655..000000000
--- a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.java
+++ /dev/null
@@ -1,223 +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 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
new file mode 100644
index 000000000..8715d2d4c
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.kt
@@ -0,0 +1,187 @@
+/*
+ * 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
+ }
+
+ 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 3bbb310c4..e4f119d8b 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.getCachedDirectoryList(context)?.forEach { directory ->
+ DictionaryInfoUtils.getCacheDirectories(context).forEach { directory ->
if (!directory.isDirectory) return@forEach
if (!hasAnythingOtherThanExtractedMainDictionary(directory)) return@forEach
val locale = DictionaryInfoUtils.getWordListIdFromFileName(directory.name).constructLocale()
@@ -35,8 +35,7 @@ fun getDictionaryLocales(context: Context): MutableSet {
val assetsDictionaryList = DictionaryInfoUtils.getAssetsDictionaryList(context)
if (assetsDictionaryList != null) {
for (dictionary in assetsDictionaryList) {
- val locale = DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dictionary)?.constructLocale() ?: continue
- locales.add(locale)
+ locales.add(DictionaryInfoUtils.extractLocaleFromAssetsDictionaryFile(dictionary))
}
}
return locales
@@ -135,4 +134,4 @@ fun cleanUnusedMainDicts(context: Context) {
}
private fun hasAnythingOtherThanExtractedMainDictionary(dir: File) =
- dir.listFiles()?.any { it.name != DictionaryInfoUtils.getExtractedMainDictFilename() } != false
+ dir.listFiles()?.any { it.name != DictionaryInfoUtils.MAIN_DICT_FILE_NAME } != 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 33ac3d5d5..71c5bda4d 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/InputMethodPicker.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/InputMethodPicker.kt
@@ -2,6 +2,7 @@
package helium314.keyboard.latin.utils
+import android.app.AlertDialog
import android.os.IBinder
import android.text.Spannable
import android.text.SpannableString
@@ -10,10 +11,10 @@ import android.text.style.RelativeSizeSpan
import android.view.WindowManager
import android.view.inputmethod.InputMethodInfo
import android.view.inputmethod.InputMethodSubtype
-import androidx.appcompat.app.AlertDialog
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 {
@@ -25,8 +26,8 @@ fun createInputMethodPickerDialog(latinIme: LatinIME, richImm: RichInputMethodMa
val enabledSubtypes = mutableListOf>()
var currentSubtypeIndex = 0
enabledImis.forEach { imi ->
- val subtypes = if (imi != thisImi) richImm.getEnabledInputMethodSubtypeList(imi, true)
- else richImm.getEnabledInputMethodSubtypeList(imi, true).sortedBy { it.displayName(latinIme).toString() }
+ val subtypes = if (imi != thisImi) richImm.getEnabledInputMethodSubtypes(imi, true)
+ else richImm.getEnabledInputMethodSubtypes(imi, true).sortedBy { it.displayName() }
if (subtypes.isEmpty()) {
enabledSubtypes.add(imi to null)
} else {
@@ -44,7 +45,7 @@ fun createInputMethodPickerDialog(latinIme: LatinIME, richImm: RichInputMethodMa
for (imiAndSubtype in enabledSubtypes) {
val (imi, subtype) = imiAndSubtype
- val subtypeName = if (imi == thisImi) subtype?.displayName(latinIme)
+ val subtypeName = if (imi == thisImi) subtype?.displayName()
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/JniUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java
index 603e32ab6..310ece153 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java
+++ b/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java
@@ -13,7 +13,6 @@ import android.text.TextUtils;
import helium314.keyboard.latin.App;
import helium314.keyboard.latin.BuildConfig;
-import helium314.keyboard.latin.settings.Defaults;
import helium314.keyboard.latin.settings.Settings;
import java.io.File;
@@ -53,17 +52,25 @@ public final class JniUtils {
.getMethod("currentApplication").invoke(null, (Object[]) null);
} catch (Exception ignored) { }
}
- if (app != null) // use the actual path if possible
+ if (app != null && app.getFilesDir() != null) // use the actual path if possible
filesDir = app.getFilesDir().getAbsolutePath();
- final File userSuppliedLibrary = new File(filesDir + File.separator + JNI_LIB_IMPORT_FILE_NAME);
- if (!BuildConfig.BUILD_TYPE.equals("nouserlib") && userSuppliedLibrary.exists()) {
+
+ 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) {
String wantedChecksum = expectedDefaultChecksum();
try {
if (app != null) {
// we want the default preferences, because storing the checksum in device protected storage is discouraged
// see https://developer.android.com/reference/android/content/Context#createDeviceProtectedStorageContext()
// if device is locked, this will throw an IllegalStateException
- wantedChecksum = KtxKt.protectedPrefs(app).getString(Settings.PREF_LIBRARY_CHECKSUM, Defaults.PREF_LIBRARY_CHECKSUM);
+ wantedChecksum = KtxKt.protectedPrefs(app).getString(Settings.PREF_LIBRARY_CHECKSUM, expectedDefaultChecksum());
}
final FileInputStream libStream = new FileInputStream(userSuppliedLibrary);
final String checksum = ChecksumCalculator.INSTANCE.checksum(libStream);
@@ -91,7 +98,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);
+ Log.w(TAG, "Could not load system glide typing library " + JNI_LIB_NAME_GOOGLE + ": " + ul.getMessage());
}
}
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 f942157ca..f7c400da2 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/LanguageOnSpacebarUtils.java
+++ b/app/src/main/java/helium314/keyboard/latin/utils/LanguageOnSpacebarUtils.java
@@ -43,16 +43,12 @@ 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(
- SubtypeLocaleUtils.getMainLayoutName(ims))) {
+ if (keyboardLanguage.equals(language) && keyboardLayout.equals(SubtypeUtilsKt.mainLayoutNameOrQwerty(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 412bb158c..00db72c87 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 56afe9ce9..13bcb24c0 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt
@@ -1,6 +1,8 @@
package helium314.keyboard.latin.utils
import android.content.Context
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.SimplePopups
+import helium314.keyboard.keyboard.internal.keyboard_parser.getOrCreate
import helium314.keyboard.latin.R
import helium314.keyboard.latin.settings.Defaults.default
import helium314.keyboard.latin.utils.LayoutType.Companion.folder
@@ -15,7 +17,7 @@ object LayoutUtils {
return context.assets.list(layoutType.folder)?.map { it.substringBefore(".") }.orEmpty()
if (locale == null)
return SubtypeSettings.getAllAvailableSubtypes()
- .mapTo(HashSet()) { it.mainLayoutName()?.substringBefore("+") ?: "qwerty" }
+ .mapTo(HashSet()) { it.mainLayoutNameOrQwerty().substringBefore("+") }
.apply { addAll(context.resources.getStringArray(R.array.predefined_layouts)) }
val layouts = SubtypeSettings.getResourceSubtypesForLocale(locale).mapNotNullTo(mutableSetOf()) { it.mainLayoutName() }
if (locale.script() == ScriptUtils.SCRIPT_LATIN)
@@ -26,6 +28,7 @@ object LayoutUtils {
fun getLMainLayoutsForLocales(locales: List, context: Context): Collection =
locales.flatMapTo(HashSet()) { getAvailableLayouts(LayoutType.MAIN, context, it) }.sorted()
+ /** gets content for built-in (non-custom) layout [layoutName], with fallback to qwerty */
fun getContent(layoutType: LayoutType, layoutName: String, context: Context): String {
val layouts = context.assets.list(layoutType.folder)!!
layouts.firstOrNull { it.startsWith("$layoutName.") }
@@ -33,4 +36,27 @@ object LayoutUtils {
val fallback = layouts.first { it.startsWith(layoutType.default) } // must exist!
return context.assets.open(layoutType.folder + File.separator + fallback).reader().readText()
}
+
+ fun getContentWithPlus(mainLayoutName: String, locale: Locale, context: Context): String {
+ val content = getContent(LayoutType.MAIN, mainLayoutName, context)
+ if (!mainLayoutName.endsWith("+"))
+ return content
+ // the stuff below will not work if we add "+" layouts in json format
+ // ideally we should serialize keyData to json to solve this
+ val rows = getSimpleRowStrings(content)
+ val localeKeyboardInfos = getOrCreate(context, locale)
+ return rows.mapIndexed { i, row ->
+ val extraKeys = localeKeyboardInfos.getExtraKeys(i + 1) ?: return@mapIndexed row
+ val rowList = row.split("\n").filterNot { it.isEmpty() }.toMutableList()
+ extraKeys.forEach { key ->
+ val popups = (key.popup as? SimplePopups)?.popupKeys?.joinToString(" ")
+ ?.takeIf { it.isNotEmpty() }?.let { " $it" } ?: ""
+ rowList.add(key.label + popups)
+ }
+ rowList.joinToString("\n")
+ }.joinToString("\n\n")
+ }
+
+ fun getSimpleRowStrings(layoutContent: String): List =
+ layoutContent.replace("\r\n", "\n").split("\\n\\s*\\n".toRegex()).filter { it.isNotBlank() }
}
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 bc40baed5..4f8686442 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtilsCustom.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtilsCustom.kt
@@ -2,6 +2,7 @@
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
@@ -10,9 +11,14 @@ 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
@@ -30,11 +36,13 @@ object LayoutUtilsCustom {
params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT)
addLocaleKeyTextsToParams(context, params, POPUP_KEYS_NORMAL)
try {
- val keys = LayoutParser.parseJsonString(layoutContent).map { row -> row.mapNotNull { it.compute(params)?.toKeyParams(params) } }
- return checkKeys(keys)
+ if (layoutContent.trimStart().startsWith("[") || layoutContent.trimStart().startsWith("//")) {
+ 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.trimStart().startsWith("[") && layoutContent.trimEnd().endsWith("]") && layoutContent.contains("},"))
+ if (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)
@@ -124,7 +132,6 @@ 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)
@@ -143,6 +150,37 @@ 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 5d47d27b8..6bd810d05 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 = null // better show nothing instead of the toolbar key label
+ if (hintLabel in toolbarKeyStrings.values || hintLabel.isNullOrEmpty())
+ return null // better show nothing instead of the toolbar key label
- return hintLabel?.let { KeySpecParser.getLabel(transformLabel(it, params)) }
+ return KeySpecParser.getLabel(transformLabel(hintLabel!!, 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 3ff53dadd..be44521d3 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/ResourceUtils.java
+++ b/app/src/main/java/helium314/keyboard/latin/utils/ResourceUtils.java
@@ -55,6 +55,15 @@ 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 df442089d..ba0fca550 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/ScriptUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/ScriptUtils.kt
@@ -162,13 +162,13 @@ object ScriptUtils {
return SCRIPT_LATIN
}
return when (language) {
- "ar", "ur", "fa" -> SCRIPT_ARABIC
+ "ar", "ckb", "ur", "fa" -> SCRIPT_ARABIC
"hy" -> SCRIPT_ARMENIAN
"bn" -> SCRIPT_BENGALI
"sr", "mk", "ru", "uk", "mn", "be", "kk", "ky", "bg", "xdq", "cv", "mhr", "mns", "dru" -> SCRIPT_CYRILLIC
"ka" -> SCRIPT_GEORGIAN
"el" -> SCRIPT_GREEK
- "iw" -> SCRIPT_HEBREW
+ "iw", "he" -> SCRIPT_HEBREW
"km" -> SCRIPT_KHMER
"lo" -> SCRIPT_LAO
"ml" -> SCRIPT_MALAYALAM
@@ -184,4 +184,12 @@ 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
new file mode 100644
index 000000000..01f4c718b
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/utils/SpacedTokens.kt
@@ -0,0 +1,38 @@
+// 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
deleted file mode 100644
index bb99355b6..000000000
--- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.java
+++ /dev/null
@@ -1,318 +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.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
new file mode 100644
index 000000000..0861bf952
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.kt
@@ -0,0 +1,190 @@
+/*
+ * 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 18739953b..00a9e752d 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt
@@ -28,9 +28,11 @@ object SubtypeSettings {
fun getEnabledSubtypes(fallback: Boolean = false): List {
if (fallback && enabledSubtypes.isEmpty())
return getDefaultEnabledSubtypes()
- return enabledSubtypes.toList()
+ return enabledSubtypes
}
+ fun isEnabled(subtype: InputMethodSubtype?): Boolean = subtype in enabledSubtypes || subtype in getDefaultEnabledSubtypes()
+
fun getAllAvailableSubtypes(): List =
resourceSubtypesByLocale.values.flatten() + additionalSubtypes
@@ -52,9 +54,8 @@ object SubtypeSettings {
fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) {
val subtype = newSubtype.toSettingsSubtype()
- val subtypes = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!
- .split(Separators.SETS).filter { it.isNotBlank() }.map { it.toSettingsSubtype() } + subtype
- val newString = subtypes.map { it.toPref() }.toSortedSet().joinToString(Separators.SETS)
+ val subtypes = createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!) + subtype
+ val newString = createPrefSubtypes(subtypes)
prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newString) }
if (newSubtype !in enabledSubtypes) {
@@ -74,10 +75,8 @@ object SubtypeSettings {
fun getSelectedSubtype(prefs: SharedPreferences): InputMethodSubtype {
val selectedSubtype = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype()
- if (selectedSubtype.isAdditionalSubtype(prefs)) {
- val selectedAdditionalSubtype = selectedSubtype.toAdditionalSubtype()
- if (selectedAdditionalSubtype != null) return selectedAdditionalSubtype
- }
+ if (selectedSubtype.isAdditionalSubtype(prefs))
+ return selectedSubtype.toAdditionalSubtype()
// no additional subtype, must be a resource subtype
val subtype = enabledSubtypes.firstOrNull { it.toSettingsSubtype() == selectedSubtype }
@@ -127,23 +126,39 @@ object SubtypeSettings {
fun getAvailableSubtypeLocales(): List = resourceSubtypesByLocale.keys.toList()
+ /**
+ * Update subtypes that contain the layout. If new name is null (layout deleted) and the
+ * subtype is now identical to a resource subtype, remove the subtype from additional subtypes.
+ */
fun onRenameLayout(type: LayoutType, from: String, to: String?, context: Context) {
val prefs = context.prefs()
+ val editor = prefs.edit() // calling apply for each separate setting would result in an invalid intermediate state
listOf(
Settings.PREF_ADDITIONAL_SUBTYPES to Defaults.PREF_ADDITIONAL_SUBTYPES,
Settings.PREF_ENABLED_SUBTYPES to Defaults.PREF_ENABLED_SUBTYPES,
Settings.PREF_SELECTED_SUBTYPE to Defaults.PREF_SELECTED_SUBTYPE
).forEach { (key, default) ->
- val new = prefs.getString(key, default)!!.split(Separators.SETS).mapTo(mutableSetOf()) {
+ val new = prefs.getString(key, default)!!.split(Separators.SETS).mapNotNullTo(mutableSetOf()) {
+ if (it.isEmpty()) return@mapNotNullTo null
val subtype = it.toSettingsSubtype()
if (subtype.layoutName(type) == from) {
- if (to == null) subtype.withoutLayout(type).toPref()
+ if (to == null) {
+ val defaultLayout = if (type !== LayoutType.MAIN) null
+ // if we just delete a main layout, we may end up with something like Hindi (QWERTY)
+ // so better replace it with a default layout for that locale
+ else resourceSubtypesByLocale[subtype.locale]?.first()?.mainLayoutName()
+ val newSubtype = if (defaultLayout == null) subtype.withoutLayout(type)
+ else subtype.withLayout(type, defaultLayout)
+ if (newSubtype.isSameAsDefault() && key == Settings.PREF_ADDITIONAL_SUBTYPES) null
+ else newSubtype.toPref()
+ }
else subtype.withLayout(type, to).toPref()
}
else subtype.toPref()
}.joinToString(Separators.SETS)
- prefs.edit().putString(key, new).apply()
+ editor.putString(key, new)
}
+ editor.apply()
if (Settings.readDefaultLayoutName(type, prefs) == from)
Settings.writeDefaultLayoutName(to, type, prefs)
reloadEnabledSubtypes(context)
@@ -151,12 +166,21 @@ object SubtypeSettings {
fun reloadEnabledSubtypes(context: Context) {
enabledSubtypes.clear()
- removeInvalidCustomSubtypes(context)
loadAdditionalSubtypes(context.prefs())
loadEnabledSubtypes(context)
- RichInputMethodManager.getInstance().refreshSubtypeCaches()
+ if (RichInputMethodManager.isInitialized())
+ RichInputMethodManager.getInstance().refreshSubtypeCaches()
}
+ fun createSettingsSubtypes(prefSubtypes: String): List =
+ prefSubtypes.split(Separators.SETS).mapNotNull {
+ if (it.isEmpty()) null
+ else it.toSettingsSubtype()
+ }
+
+ fun createPrefSubtypes(subtypes: Collection): String =
+ subtypes.map { it.toPref() }.toSortedSet().joinToString(Separators.SETS)
+
fun init(context: Context) {
SubtypeLocaleUtils.init(context) // necessary to get the correct getKeyboardLayoutSetName
@@ -165,7 +189,6 @@ object SubtypeSettings {
reloadSystemLocales(context)
loadResourceSubtypes(context.resources)
- removeInvalidCustomSubtypes(context)
loadAdditionalSubtypes(context.prefs())
loadEnabledSubtypes(context)
}
@@ -193,23 +216,6 @@ 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")
- Settings.writePrefAdditionalSubtypes(prefs, additionalSubtypes.filterNot { it in subtypesToRemove }.joinToString(Separators.SETS))
- }
-
private fun loadAdditionalSubtypes(prefs: SharedPreferences) {
additionalSubtypes.clear()
val additionalSubtypeString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
@@ -220,15 +226,11 @@ object SubtypeSettings {
// requires loadResourceSubtypes to be called before
private fun loadEnabledSubtypes(context: Context) {
val prefs = context.prefs()
- val settingsSubtypes = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!
- .split(Separators.SETS).filter { it.isNotEmpty() }.map { it.toSettingsSubtype() }
+ val settingsSubtypes = createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!)
for (settingsSubtype in settingsSubtypes) {
if (settingsSubtype.isAdditionalSubtype(prefs)) {
- val additionalSubtype = settingsSubtype.toAdditionalSubtype()
- if (additionalSubtype != null) {
- enabledSubtypes.add(additionalSubtype)
- continue
- }
+ enabledSubtypes.add(settingsSubtype.toAdditionalSubtype())
+ continue
}
val subtypesForLocale = resourceSubtypesByLocale[settingsSubtype.locale]
if (subtypesForLocale == null) {
@@ -241,7 +243,7 @@ object SubtypeSettings {
continue
}
- val subtype = subtypesForLocale.firstOrNull { SubtypeLocaleUtils.getMainLayoutName(it) == (settingsSubtype.mainLayoutName() ?: SubtypeLocaleUtils.QWERTY) }
+ val subtype = subtypesForLocale.firstOrNull { it.mainLayoutNameOrQwerty() == (settingsSubtype.mainLayoutName() ?: SubtypeLocaleUtils.QWERTY) }
if (subtype == null) {
val message = "subtype $settingsSubtype could not be loaded"
Log.w(TAG, message)
@@ -258,12 +260,11 @@ object SubtypeSettings {
/** @return whether pref was changed */
private fun removeEnabledSubtype(prefs: SharedPreferences, subtype: SettingsSubtype): Boolean {
- val oldSubtypes = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!
- .split(Separators.SETS).filter { it.isNotEmpty() }.map { it.toSettingsSubtype() }
+ val oldSubtypes = createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!)
val newSubtypes = oldSubtypes - subtype
if (oldSubtypes == newSubtypes)
return false // already removed
- prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newSubtypes.joinToString(Separators.SETS) { it.toPref() }) }
+ prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, createPrefSubtypes(newSubtypes)) }
if (subtype == prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype()) {
// switch subtype if the currently used one has been disabled
try {
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 a210174ac..1b69075c3 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt
@@ -1,6 +1,5 @@
package helium314.keyboard.latin.utils
-import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import android.os.Build
@@ -10,7 +9,6 @@ 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
@@ -30,6 +28,8 @@ 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,15 +64,6 @@ 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 048a7410b..a2d839266 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt
@@ -9,9 +9,13 @@ 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 {
@@ -46,17 +50,27 @@ 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)
val oldAdditionalSubtypesString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
- val oldAdditionalSubtypes = createAdditionalSubtypes(oldAdditionalSubtypesString)
- val newAdditionalSubtypes = oldAdditionalSubtypes.filter { it != subtype }
- val newAdditionalSubtypesString = createPrefSubtypes(newAdditionalSubtypes)
- Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString)
+ val oldAdditionalSubtypes = SubtypeSettings.createSettingsSubtypes(oldAdditionalSubtypesString)
+ val settingsSubtype = subtype.toSettingsSubtype()
+ val newAdditionalSubtypes = oldAdditionalSubtypes.filter { it != settingsSubtype }
+ val newAdditionalSubtypesString = SubtypeSettings.createPrefSubtypes(newAdditionalSubtypes)
+ prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, newAdditionalSubtypesString).apply()
}
// updates additional subtypes, enabled subtypes, and selected subtype
@@ -66,38 +80,39 @@ object SubtypeUtilsAdditional {
val isSelected = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype() == from
val isEnabled = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!.split(Separators.SETS)
.any { it.toSettingsSubtype() == from }
- val new = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
- .split(Separators.SETS).mapNotNullTo(sortedSetOf()) {
- if (it == from.toPref()) null else it
- } + to.toPref()
- prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, new.joinToString(Separators.SETS)).apply()
-
- val fromSubtype = from.toAdditionalSubtype() // will be null if we edit a resource subtype
- val toSubtype = to.toAdditionalSubtype() // should never be null
- if (isSelected && toSubtype != null) {
- SubtypeSettings.setSelectedSubtype(prefs, toSubtype)
+ val additionalSubtypes = SubtypeSettings.createSettingsSubtypes(prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!)
+ .toMutableList()
+ additionalSubtypes.remove(from)
+ if (!to.isSameAsDefault()) {
+ // We only add the "to" subtype if it's not equal to a resource subtype.
+ // This means we make additional subtype disappear as magically as it was added if all settings are default.
+ // If we don't do this, enabling the base subtype will result in the additional subtype being enabled,
+ // as both have the same settingsSubtype.
+ additionalSubtypes.add(to)
}
- if (fromSubtype != null && isEnabled && toSubtype != null) {
- SubtypeSettings.removeEnabledSubtype(context, fromSubtype)
- SubtypeSettings.addEnabledSubtype(prefs, toSubtype)
+ val editor = prefs.edit()
+ editor.putString(Settings.PREF_ADDITIONAL_SUBTYPES, SubtypeSettings.createPrefSubtypes(additionalSubtypes))
+ if (isSelected) {
+ editor.putString(Settings.PREF_SELECTED_SUBTYPE, to.toPref())
}
+ if (isEnabled) {
+ val enabled = SubtypeSettings.createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!)
+ .toMutableList()
+ enabled.remove(from)
+ enabled.add(to)
+ editor.putString(Settings.PREF_ENABLED_SUBTYPES, SubtypeSettings.createPrefSubtypes(enabled))
+ }
+ editor.apply()
+ SubtypeSettings.reloadEnabledSubtypes(context)
}
- fun createAdditionalSubtypes(prefSubtypes: String): List {
- if (prefSubtypes.isEmpty())
- return emptyList()
- return prefSubtypes.split(Separators.SETS).mapNotNull { it.toSettingsSubtype().toAdditionalSubtype() }
- }
-
- fun createPrefSubtypes(subtypes: Collection): String {
- if (subtypes.isEmpty())
- return ""
- return subtypes.joinToString(Separators.SETS) { it.toSettingsSubtype().toPref() }
- }
+ fun createAdditionalSubtypes(prefSubtypes: String): List =
+ prefSubtypes.split(Separators.SETS).mapNotNull {
+ if (it.isEmpty()) null
+ else it.toSettingsSubtype().toAdditionalSubtype()
+ }
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
}
@@ -146,15 +161,17 @@ 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/Timestamp.kt b/app/src/main/java/helium314/keyboard/latin/utils/Timestamp.kt
new file mode 100644
index 000000000..ebe2bd0e8
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/utils/Timestamp.kt
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-3.0-only
+package helium314.keyboard.latin.utils
+
+import android.content.Context
+import helium314.keyboard.latin.settings.Defaults
+import helium314.keyboard.latin.settings.Settings
+import java.text.SimpleDateFormat
+import java.util.Calendar
+
+fun getTimestamp(context: Context): String {
+ val format = context.prefs().getString(Settings.PREF_TIMESTAMP_FORMAT, Defaults.PREF_TIMESTAMP_FORMAT)
+ val formatter = runCatching { SimpleDateFormat(format, Settings.getValues().mLocale) }.getOrNull()
+ ?: SimpleDateFormat(Defaults.PREF_TIMESTAMP_FORMAT, Settings.getValues().mLocale)
+ return formatter.format(Calendar.getInstance().time)
+}
+
+fun checkTimestampFormat(format: String) = runCatching { SimpleDateFormat(format, Settings.getValues().mLocale) }.isSuccess
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 1293d33a4..d9428b1f7 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, iconsSet: KeyboardIconsSet, key: ToolbarKey): ImageButton {
+fun createToolbarKey(context: Context, 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, iconsSet: KeyboardIconsSet, key: ToolbarK
if (contentDescriptionId != 0)
button.contentDescription = context.getString(contentDescriptionId)
setToolbarButtonActivatedState(button)
- button.setImageDrawable(iconsSet.getNewDrawable(key.name, context))
+ button.setImageDrawable(KeyboardIconsSet.instance.getNewDrawable(key.name, context))
return button
}
@@ -115,6 +115,10 @@ 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/Misc.kt b/app/src/main/java/helium314/keyboard/settings/Misc.kt
index 05925afb3..2ca4add2c 100644
--- a/app/src/main/java/helium314/keyboard/settings/Misc.kt
+++ b/app/src/main/java/helium314/keyboard/settings/Misc.kt
@@ -1,13 +1,18 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
+import android.graphics.drawable.VectorDrawable
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
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.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -17,8 +22,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
-
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.toBitmap
+import androidx.core.util.TypedValueCompat
@Composable
fun WithSmallTitle(
@@ -31,6 +43,19 @@ fun WithSmallTitle(
}
}
+/** Icon if resource is a vector image, (bitmap) Image otherwise */
+@Composable
+fun IconOrImage(@DrawableRes resId: Int, name: String?, sizeDp: Float) {
+ val ctx = LocalContext.current
+ val drawable = ContextCompat.getDrawable(ctx, resId)
+ if (drawable is VectorDrawable)
+ Icon(painterResource(resId), name, Modifier.size(sizeDp.dp))
+ else {
+ val px = TypedValueCompat.dpToPx(sizeDp, ctx.resources.displayMetrics).toInt()
+ Image(drawable!!.toBitmap(px, px).asImageBitmap(), name)
+ }
+}
+
@Composable
fun DropDownField(
items: List,
@@ -67,3 +92,5 @@ fun DropDownField(
}
}
}
+
+val contentTextDirectionStyle = TextStyle(textDirection = TextDirection.Content)
diff --git a/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt b/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
index 7184d981c..66ba49f64 100644
--- a/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
@@ -6,9 +6,13 @@ 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
@@ -20,6 +24,7 @@ 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
@@ -33,7 +38,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
@@ -59,36 +63,42 @@ fun SearchSettingsScreen(
content = {
if (content != null) content()
else {
- 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()
+ 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()
+ }
}
}
}
+ // 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) },
@@ -103,96 +113,99 @@ fun SearchScreen(
title: @Composable () -> Unit,
filteredItems: (String) -> List,
itemContent: @Composable (T) -> Unit,
+ icon: @Composable (() -> Unit)? = null,
menu: List Unit>>? = null,
content: @Composable (ColumnScope.() -> Unit)? = null,
) {
- var searchText by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
- Column(Modifier.fillMaxSize()) {
- var showSearch by remember { mutableStateOf(false) }
+ // searchText and showSearch should have the same remember or rememberSaveable
+ // saveable survives orientation changes and switching between screens, but shows the
+ // 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)) {
- 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 = {
- IconButton(onClick = { setShowSearch(!showSearch) })
- { SearchIcon() }
- 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 = 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() }
+ )
+ }
}
}
- }
- },
- )
- 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
+ },
+ )
+ 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)
+ }
+ 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)
+ }
+ }
}
}
}
@@ -227,7 +240,8 @@ fun ExpandableSearchField(
else onSearchChange(TextFieldValue())
}) { CloseIcon(android.R.string.cancel) } },
singleLine = true,
- colors = colors
+ colors = colors,
+ 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 498b7293c..19571b21c 100644
--- a/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt
+++ b/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt
@@ -5,32 +5,41 @@ 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.core.view.ViewCompat
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
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
@@ -53,11 +62,13 @@ import java.util.zip.ZipOutputStream
class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
private val prefs by lazy { this.prefs() }
val prefChanged = MutableStateFlow(0) // simple counter, as the only relevant information is that something changed
+ fun prefChanged() = prefChanged.value++
private val dictUriFlow = MutableStateFlow(null)
private val cachedDictionaryFile by lazy { File(this.cacheDir.path + File.separator + "temp_dict") }
private val crashReportFiles = MutableStateFlow>(emptyList())
private var paused = true
+ @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Settings.getValues() == null) {
@@ -65,21 +76,9 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
Settings.getInstance().loadSettings(this, resources.configuration.locale(), inputAttributes)
}
ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute { cleanUnusedMainDicts(this) }
- if (BuildConfig.DEBUG || DebugFlags.DEBUG_ENABLED)
- crashReportFiles.value = findCrashReports()
- setSystemBarIconColor()
+ crashReportFiles.value = findCrashReports(!BuildConfig.DEBUG && !DebugFlags.DEBUG_ENABLED)
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
@@ -97,12 +96,47 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
|| !UncachedInputMethodManagerUtils.isThisImeEnabled(this, imm)
) }
if (spellchecker)
- Column { // lazy way of implementing spell checker settings
- settingsContainer[Settings.PREF_USE_CONTACTS]!!.Preference()
- settingsContainer[Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE]!!.Preference()
+ 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()
+ }
}
- 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 },
@@ -110,26 +144,6 @@ 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)
- }
}
}
}
@@ -142,6 +156,8 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
}
intent = null
}
+
+ enableEdgeToEdge()
}
override fun onStart() {
@@ -174,15 +190,18 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
KeyboardSwitcher.getInstance().setThemeNeedsReload()
}
- private fun findCrashReports(): List {
- // find crash report files
- val dir: File = getExternalFilesDir(null) ?: return emptyList()
- val allFiles = dir.listFiles() ?: return emptyList()
+ 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
return allFiles.filter { it.name.startsWith("crash_report") }
}
private fun saveCrashReports(uri: Uri) {
- val files = findCrashReports()
+ val files = findCrashReports(false)
if (files.isEmpty()) return
try {
contentResolver.openOutputStream(uri)?.use {
@@ -205,23 +224,6 @@ 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
@@ -230,12 +232,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?) {
- prefChanged.value++
+ prefChanged()
}
}
diff --git a/app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt b/app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt
index f3773125e..7a694070f 100644
--- a/app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt
+++ b/app/src/main/java/helium314/keyboard/settings/SettingsNavHost.kt
@@ -1,16 +1,22 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.settings
+import android.provider.Settings
+import android.provider.Settings.Global
+import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
+import helium314.keyboard.latin.settings.SettingsSubtype.Companion.toSettingsSubtype
import helium314.keyboard.settings.screens.AboutScreen
import helium314.keyboard.settings.screens.AdvancedSettingsScreen
import helium314.keyboard.settings.screens.AppearanceScreen
@@ -24,6 +30,7 @@ import helium314.keyboard.settings.screens.PersonalDictionariesScreen
import helium314.keyboard.settings.screens.PersonalDictionaryScreen
import helium314.keyboard.settings.screens.PreferencesScreen
import helium314.keyboard.settings.screens.SecondaryLayoutScreen
+import helium314.keyboard.settings.screens.SubtypeScreen
import helium314.keyboard.settings.screens.TextCorrectionScreen
import helium314.keyboard.settings.screens.ToolbarScreen
import kotlinx.coroutines.CoroutineScope
@@ -41,6 +48,10 @@ fun SettingsNavHost(
val dir = if (LocalLayoutDirection.current == LayoutDirection.Ltr) 1 else -1
val target = SettingsDestination.navTarget.collectAsState()
+ // duration does not change when system setting changes, but that's rare enough to not care
+ val duration = (250 * Settings.System.getFloat(LocalContext.current.contentResolver, Global.TRANSITION_ANIMATION_SCALE, 1f)).toInt()
+ val animation = tween(durationMillis = duration)
+
fun goBack() {
if (!navController.popBackStack()) onClickBack()
}
@@ -48,10 +59,10 @@ fun SettingsNavHost(
NavHost(
navController = navController,
startDestination = startDestination ?: SettingsDestination.Settings,
- enterTransition = { slideInHorizontally(initialOffsetX = { +it * dir }) },
- exitTransition = { slideOutHorizontally(targetOffsetX = { -it * dir }) },
- popEnterTransition = { slideInHorizontally(initialOffsetX = { -it * dir }) },
- popExitTransition = { slideOutHorizontally(targetOffsetX = { +it * dir }) }
+ enterTransition = { slideInHorizontally(initialOffsetX = { +it * dir }, animationSpec = animation) },
+ exitTransition = { slideOutHorizontally(targetOffsetX = { -it * dir }, animationSpec = animation) },
+ popEnterTransition = { slideInHorizontally(initialOffsetX = { -it * dir }, animationSpec = animation) },
+ popExitTransition = { slideOutHorizontally(targetOffsetX = { +it * dir }, animationSpec = animation) }
) {
composable(SettingsDestination.Settings) {
MainSettingsScreen(
@@ -117,6 +128,9 @@ fun SettingsNavHost(
composable(SettingsDestination.ColorsNight + "{theme}") {
ColorsScreen(isNight = true, theme = it.arguments?.getString("theme"), onClickBack = ::goBack)
}
+ composable(SettingsDestination.Subtype + "{subtype}") {
+ SubtypeScreen(initialSubtype = it.arguments?.getString("subtype")!!.toSettingsSubtype(), onClickBack = ::goBack)
+ }
}
if (target.value != SettingsDestination.Settings/* && target.value != navController.currentBackStackEntry?.destination?.route*/)
navController.navigate(route = target.value)
@@ -137,6 +151,7 @@ object SettingsDestination {
const val PersonalDictionaries = "personal_dictionaries"
const val PersonalDictionary = "personal_dictionary/"
const val Languages = "languages"
+ const val Subtype = "subtype/"
const val Layouts = "layouts"
const val Dictionaries = "dictionaries"
val navTarget = MutableStateFlow(Settings)
diff --git a/app/src/main/java/helium314/keyboard/settings/Theme.kt b/app/src/main/java/helium314/keyboard/settings/Theme.kt
index e8924a300..32d8121b9 100644
--- a/app/src/main/java/helium314/keyboard/settings/Theme.kt
+++ b/app/src/main/java/helium314/keyboard/settings/Theme.kt
@@ -12,9 +12,6 @@ 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
@@ -36,20 +33,9 @@ fun Theme(dark: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit
MaterialTheme(
colorScheme = colorScheme,
typography = Typography(
- 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))
- )
+ titleLarge = material3.titleLarge.copy(fontWeight = FontWeight.Bold),
+ titleMedium = material3.titleMedium.copy(fontWeight = FontWeight.Bold),
+ titleSmall = material3.titleSmall.copy(fontWeight = FontWeight.Bold)
),
//shapes = Shapes(),
content = content
diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/ColorPickerDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/ColorPickerDialog.kt
index 15e926acb..d1771b61b 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/ColorPickerDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/ColorPickerDialog.kt
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -24,6 +25,7 @@ import androidx.compose.ui.graphics.PaintingStyle
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
@@ -109,8 +111,12 @@ fun ColorPickerDialog(
)
TextField(
value = textValue,
- // todo: KeyboardType.Password is a crappy way of avoiding suggestions... is there really no way in compose?
- keyboardOptions = KeyboardOptions(autoCorrectEnabled = false, keyboardType = KeyboardType.Password),
+ keyboardOptions = KeyboardOptions(
+ autoCorrectEnabled = false,
+ keyboardType = KeyboardType.Password, // todo: KeyboardType.Password is a crappy way of avoiding suggestions... is there really no way in compose?
+ imeAction = ImeAction.Done,
+ ),
+ keyboardActions = KeyboardActions(onDone = { onDismissRequest(); onConfirmed(controller.selectedColor.value.toArgb()) }),
onValueChange = {
textValue = it
val androidColor = runCatching { android.graphics.Color.parseColor("#${it.text}") }.getOrNull()
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 ac97743f8..24101e86e 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt
@@ -60,6 +60,7 @@ import helium314.keyboard.settings.Theme
import helium314.keyboard.settings.filePicker
import helium314.keyboard.settings.previewDark
import helium314.keyboard.settings.screens.SaveThoseColors
+import helium314.keyboard.settings.contentTextDirectionStyle
import kotlinx.coroutines.launch
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
@@ -187,7 +188,8 @@ private fun AddColorRow(onDismissRequest: () -> Unit, userColors: Collection
@@ -108,10 +114,9 @@ fun LayoutEditDialog(
}
valid && nameValid // don't allow saving with invalid name, but inform user about issues with layout content
},
- // 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?
+ 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
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 7355c7e41..ace5aafb8 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutPickerDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutPickerDialog.kt
@@ -50,6 +50,7 @@ import helium314.keyboard.settings.EditButton
import helium314.keyboard.settings.Setting
import helium314.keyboard.settings.SettingsActivity
import helium314.keyboard.settings.Theme
+import helium314.keyboard.settings.contentTextDirectionStyle
import helium314.keyboard.settings.layoutFilePicker
import helium314.keyboard.settings.layoutIntent
import helium314.keyboard.settings.previewDark
@@ -140,7 +141,8 @@ private fun AddLayoutRow(onNewLayout: (String) -> Unit, layoutType: LayoutType,
value = textValue,
onValueChange = { textValue = it },
modifier = Modifier.weight(1f),
- singleLine = true
+ singleLine = true,
+ textStyle = contentTextDirectionStyle,
)
EditButton(textValue.text.isNotEmpty() && LayoutUtilsCustom.getLayoutName(textValue.text, layoutType) !in userLayouts) {
onNewLayout(textValue.text)
@@ -202,6 +204,7 @@ 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 2bf3da39c..1405c71ae 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/NewDictionaryDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/NewDictionaryDialog.kt
@@ -32,6 +32,7 @@ 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(
@@ -52,10 +53,10 @@ fun NewDictionaryDialog(
val locales = SubtypeSettings.getAvailableSubtypeLocales()
.filter { it.script() == dictLocale.script() || it.script() == mainLocale?.script() }
.sortedWith(comparer)
- val cacheDir = DictionaryInfoUtils.getAndCreateCacheDirectoryForLocale(locale, ctx)
+ val cacheDir = DictionaryInfoUtils.getCacheDirectoryForLocale(locale, ctx)
val dictFile = File(cacheDir, header.mIdString.substringBefore(":") + "_" + DictionaryInfoUtils.USER_DICTIONARY_SUFFIX)
val type = header.mIdString.substringBefore(":")
- val info = header.info(ctx.resources.configuration.locale())
+ val info = header.info(LocalConfiguration.current.locale())
ThreeButtonAlertDialog(
onDismissRequest = { onDismissRequest(); cachedFile.delete() },
onConfirmed = {
@@ -64,7 +65,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.getExtractedMainDictFilename())
+ val internalMainDictFile = File(cacheDir, DictionaryInfoUtils.MAIN_DICT_FILE_NAME)
internalMainDictFile.delete()
}
val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)
@@ -80,7 +81,7 @@ fun NewDictionaryDialog(
selectedItem = locale,
onSelected = { locale = it },
items = locales
- ) { Text(it.localizedDisplayName(ctx)) }
+ ) { Text(it.localizedDisplayName(ctx.resources)) }
}
if (locale.script() != dictLocale.script()) {
// whatever, still allow it if the user wants
@@ -92,7 +93,7 @@ fun NewDictionaryDialog(
)
}
if (dictFile.exists()) {
- val oldInfo = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(dictFile, 0, dictFile.length())?.info(ctx.resources.configuration.locale())
+ val oldInfo = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(dictFile, 0, dictFile.length())?.info(LocalConfiguration.current.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/TextInputDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/TextInputDialog.kt
index d23f76daa..fa9b8a105 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/TextInputDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/TextInputDialog.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.DialogProperties
import helium314.keyboard.settings.Theme
+import helium314.keyboard.settings.contentTextDirectionStyle
import helium314.keyboard.settings.previewDark
// mostly taken from StreetComplete / SCEE
@@ -76,7 +77,8 @@ fun TextInputDialog(
.focusRequester(focusRequester),
label = textInputLabel,
keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
- singleLine = singleLine
+ singleLine = singleLine,
+ textStyle = contentTextDirectionStyle,
)
},
properties = properties,
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 2b34bcd89..cc20a4e63 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.titleLarge) {
+ CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleMedium) {
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 8b70448b8..0f500cdf9 100644
--- a/app/src/main/java/helium314/keyboard/settings/preferences/BackgroundImagePreference.kt
+++ b/app/src/main/java/helium314/keyboard/settings/preferences/BackgroundImagePreference.kt
@@ -105,6 +105,7 @@ 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 675dc2c9d..e156a5d21 100644
--- a/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt
+++ b/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt
@@ -3,6 +3,8 @@ 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
@@ -13,13 +15,12 @@ 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
@@ -51,10 +52,10 @@ fun BackupRestorePreference(setting: Setting) {
val prefs = ctx.prefs()
var error: String? by rememberSaveable { mutableStateOf(null) }
val backupFilePatterns by lazy { listOf(
- "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(),
+ "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(),
"custom_background_image.*".toRegex(),
"custom_font".toRegex(),
) }
@@ -131,13 +132,11 @@ 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 targetFileName = upgradeFileNames(adjustedName)
- val file = File(deviceProtectedFilesDir, targetFileName)
+ val file = File(deviceProtectedFilesDir, adjustedName)
FileUtils.copyStreamToNewFile(zip, file)
}
} else if (backupFilePatterns.any { entry!!.name.matches(it) }) {
- val targetFileName = upgradeFileNames(entry.name)
- val file = File(filesDir, targetFileName)
+ val file = File(filesDir, entry.name)
FileUtils.copyStreamToNewFile(zip, file)
} else if (entry.name == PREFS_FILE_NAME) {
val prefLines = String(zip.readBytes()).split("\n")
@@ -154,6 +153,9 @@ 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)
@@ -168,7 +170,9 @@ fun BackupRestorePreference(setting: Setting) {
val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)
ctx.getActivity()?.sendBroadcast(newDictBroadcast)
LayoutUtilsCustom.onLayoutFileChanged()
- (ctx.getActivity() as? SettingsActivity)?.prefChanged?.value = 210 // for settings reload
+ LayoutUtilsCustom.removeMissingLayouts(ctx)
+ (ctx.getActivity() as? SettingsActivity)?.prefChanged()
+ SupportedEmojis.load(ctx)
KeyboardSwitcher.getInstance().setThemeNeedsReload()
}
Preference(name = setting.title, onClick = { showDialog = true })
@@ -253,38 +257,5 @@ 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/LoadGestureLibPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/LoadGestureLibPreference.kt
index 591d4a5c7..5b51bdbe2 100644
--- a/app/src/main/java/helium314/keyboard/settings/preferences/LoadGestureLibPreference.kt
+++ b/app/src/main/java/helium314/keyboard/settings/preferences/LoadGestureLibPreference.kt
@@ -35,10 +35,13 @@ fun LoadGestureLibPreference(setting: Setting) {
val abi = Build.SUPPORTED_ABIS[0]
val libFile = File(ctx.filesDir?.absolutePath + File.separator + JniUtils.JNI_LIB_IMPORT_FILE_NAME)
fun renameToLibFileAndRestart(file: File, checksum: String) {
+ libFile.setWritable(true)
libFile.delete()
- // store checksum in default preferences (soo JniUtils)
+ // store checksum in default preferences (see JniUtils)
prefs.edit().putString(Settings.PREF_LIBRARY_CHECKSUM, checksum).commit()
- file.renameTo(libFile)
+ file.copyTo(libFile)
+ libFile.setReadOnly()
+ file.delete()
Runtime.getRuntime().exit(0) // exit will restart the app, so library will be loaded
}
var tempFilePath: String? by rememberSaveable { mutableStateOf(null) }
diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/Preference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/Preference.kt
index 33cd45b63..63fc46922 100644
--- a/app/src/main/java/helium314/keyboard/settings/preferences/Preference.kt
+++ b/app/src/main/java/helium314/keyboard/settings/preferences/Preference.kt
@@ -10,7 +10,6 @@ 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.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
@@ -29,6 +28,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import helium314.keyboard.latin.R
+import helium314.keyboard.settings.IconOrImage
import helium314.keyboard.settings.Theme
import helium314.keyboard.settings.previewDark
@@ -64,12 +64,12 @@ fun Preference(
.fillMaxWidth()
.clickable { onClick() }
.heightIn(min = 44.dp)
- .padding(12.dp),
+ .padding(vertical = 10.dp, horizontal = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (icon != null)
- Icon(painterResource(icon), name, modifier = Modifier.size(36.dp))
+ IconOrImage(icon, name, 32f)
Column(modifier = Modifier.weight(1f)) {
Text(text = name, style = MaterialTheme.typography.bodyLarge)
if (description != null) {
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 1eae33b41..97138672a 100644
--- a/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt
+++ b/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt
@@ -26,7 +26,8 @@ fun SliderPreference(
default: T,
range: ClosedFloatingPointRange,
stepSize: Int? = null,
- onValueChanged: (Float) -> Unit = { },
+ onValueChanged: (Float?) -> Unit = { },
+ onConfirmed: (T) -> Unit = { },
) {
val ctx = LocalContext.current
val prefs = ctx.prefs()
@@ -48,8 +49,13 @@ fun SliderPreference(
SliderDialog(
onDismissRequest = { showDialog = false },
onDone = {
- if (default is Int) prefs.edit().putInt(key, it.toInt()).apply()
- else prefs.edit().putFloat(key, it).apply()
+ if (default is Int) {
+ prefs.edit().putInt(key, it.toInt()).apply()
+ onConfirmed(it.toInt() as T)
+ } else {
+ prefs.edit().putFloat(key, it).apply()
+ onConfirmed(it as T)
+ }
},
initialValue = initialValue.toFloat(),
range = range,
@@ -59,7 +65,7 @@ fun SliderPreference(
},
onValueChanged = onValueChanged,
showDefault = true,
- onDefault = { prefs.edit().remove(key).apply() },
+ onDefault = { prefs.edit().remove(key).apply(); onConfirmed(default) },
intermediateSteps = stepSize?.let {
// this is not nice, but slider wants it like this...
((range.endInclusive - range.start) / it - 1).toInt()
diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/TextInputPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/TextInputPreference.kt
new file mode 100644
index 000000000..3bf2ce8e9
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/settings/preferences/TextInputPreference.kt
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: GPL-3.0-only
+package helium314.keyboard.settings.preferences
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.platform.LocalContext
+import helium314.keyboard.keyboard.KeyboardSwitcher
+import helium314.keyboard.latin.utils.prefs
+import helium314.keyboard.settings.Setting
+import helium314.keyboard.settings.dialogs.TextInputDialog
+
+@Composable
+fun TextInputPreference(setting: Setting, default: String, checkTextValid: (String) -> Boolean = { true }) {
+ var showDialog by rememberSaveable { mutableStateOf(false) }
+ val prefs = LocalContext.current.prefs()
+ Preference(
+ name = setting.title,
+ onClick = { showDialog = true },
+ description = prefs.getString(setting.key, default)?.takeIf { it.isNotEmpty() }
+ )
+ if (showDialog) {
+ TextInputDialog(
+ onDismissRequest = { showDialog = false },
+ onConfirmed = {
+ prefs.edit().putString(setting.key, it).apply()
+ KeyboardSwitcher.getInstance().setThemeNeedsReload()
+ },
+ initialText = prefs.getString(setting.key, default) ?: "",
+ title = { Text(setting.title) },
+ checkTextValid = checkTextValid
+ )
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt
index 0b827952b..36a5cb734 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/AboutScreen.kt
@@ -2,6 +2,7 @@
package helium314.keyboard.settings.screens
import android.app.Activity
+import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.text.method.LinkMovementMethod
@@ -10,7 +11,6 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AlertDialog
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -54,7 +54,7 @@ fun AboutScreen(
SettingsWithoutKey.LICENSE,
SettingsWithoutKey.HIDDEN_FEATURES,
SettingsWithoutKey.GITHUB,
- SettingsWithoutKey.SAVE_LOG
+ SettingsWithoutKey.SAVE_LOG,
)
SearchSettingsScreen(
onClickBack = onClickBack,
@@ -69,7 +69,7 @@ fun createAboutSettings(context: Context) = listOf(
name = it.title,
description = it.description,
onClick = { },
- icon = R.drawable.ic_launcher_foreground // use the bitmap trick here if we really want the colored icon
+ icon = R.mipmap.ic_launcher_round
)
},
Setting(context, SettingsWithoutKey.VERSION, R.string.version) {
@@ -150,7 +150,11 @@ fun createAboutSettings(context: Context) = listOf(
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
scope.launch(Dispatchers.IO) {
ctx.getActivity()?.contentResolver?.openOutputStream(uri)?.use { os ->
- os.bufferedWriter().use { it.write(Log.getLog().joinToString("\n")) }
+ os.writer().use {
+ val logcat = Runtime.getRuntime().exec("logcat -d -b all *:W").inputStream.use { it.reader().readText() }
+ val internal = Log.getLog().joinToString("\n")
+ it.write(logcat + "\n\n" + internal)
+ }
}
}
}
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt
index 3605a4228..e57f6a5ca 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/AdvancedScreen.kt
@@ -17,6 +17,7 @@ import androidx.compose.ui.tooling.preview.Preview
import helium314.keyboard.keyboard.KeyboardActionListener
import helium314.keyboard.keyboard.KeyboardLayoutSet
import helium314.keyboard.keyboard.KeyboardSwitcher
+import helium314.keyboard.keyboard.emoji.SupportedEmojis
import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_ALL
import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_MAIN
import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_MORE
@@ -29,6 +30,7 @@ import helium314.keyboard.latin.common.splitOnWhitespace
import helium314.keyboard.latin.settings.DebugSettings
import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings
+import helium314.keyboard.latin.utils.checkTimestampFormat
import helium314.keyboard.latin.utils.prefs
import helium314.keyboard.settings.NextScreenIcon
import helium314.keyboard.settings.SettingsContainer
@@ -45,6 +47,7 @@ import helium314.keyboard.settings.Theme
import helium314.keyboard.settings.dialogs.TextInputDialog
import helium314.keyboard.settings.preferences.BackupRestorePreference
import helium314.keyboard.settings.preferences.LoadGestureLibPreference
+import helium314.keyboard.settings.preferences.TextInputPreference
import helium314.keyboard.settings.previewDark
@Composable
@@ -66,10 +69,12 @@ fun AdvancedSettingsScreen(
Settings.PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY,
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) Settings.PREF_SHOW_SETUP_WIZARD_ICON else null,
Settings.PREF_ABC_AFTER_SYMBOL_SPACE,
+ Settings.PREF_ABC_AFTER_NUMPAD_SPACE,
Settings.PREF_ABC_AFTER_EMOJI,
Settings.PREF_ABC_AFTER_CLIP,
Settings.PREF_CUSTOM_CURRENCY_KEY,
Settings.PREF_MORE_POPUP_KEYS,
+ Settings.PREF_TIMESTAMP_FORMAT,
SettingsWithoutKey.BACKUP_RESTORE,
if (BuildConfig.DEBUG || prefs.getBoolean(DebugSettings.PREF_SHOW_DEBUG_SETTINGS, Defaults.PREF_SHOW_DEBUG_SETTINGS))
SettingsWithoutKey.DEBUG_SETTINGS else null,
@@ -154,6 +159,11 @@ fun createAdvancedSettings(context: Context) = listOf(
{
SwitchPreference(it, Defaults.PREF_ABC_AFTER_SYMBOL_SPACE)
},
+ Setting(context, Settings.PREF_ABC_AFTER_NUMPAD_SPACE,
+ R.string.switch_keyboard_after, R.string.after_numpad_and_space)
+ {
+ SwitchPreference(it, Defaults.PREF_ABC_AFTER_NUMPAD_SPACE)
+ },
Setting(context, Settings.PREF_ABC_AFTER_EMOJI, R.string.switch_keyboard_after, R.string.after_emoji) {
SwitchPreference(it, Defaults.PREF_ABC_AFTER_EMOJI)
},
@@ -189,6 +199,9 @@ fun createAdvancedSettings(context: Context) = listOf(
Setting(context, SettingsWithoutKey.BACKUP_RESTORE, R.string.backup_restore_title) {
BackupRestorePreference(it)
},
+ Setting(context, Settings.PREF_TIMESTAMP_FORMAT, R.string.timestamp_format_title) {
+ TextInputPreference(it, Defaults.PREF_TIMESTAMP_FORMAT) { checkTimestampFormat(it) }
+ },
Setting(context, SettingsWithoutKey.DEBUG_SETTINGS, R.string.debug_settings_title) {
Preference(
name = it.title,
@@ -196,6 +209,7 @@ fun createAdvancedSettings(context: Context) = listOf(
) { NextScreenIcon() }
},
Setting(context, Settings.PREF_EMOJI_MAX_SDK, R.string.prefs_key_emoji_max_sdk) { setting ->
+ val ctx = LocalContext.current
SliderPreference(
name = setting.title,
key = setting.key,
@@ -221,7 +235,10 @@ fun createAdvancedSettings(context: Context) = listOf(
else -> "version unknown"
}
},
- onValueChanged = { KeyboardSwitcher.getInstance().setThemeNeedsReload() }
+ onConfirmed = {
+ SupportedEmojis.load(ctx)
+ KeyboardSwitcher.getInstance().setThemeNeedsReload()
+ }
)
},
Setting(context, Settings.PREF_URL_DETECTION, R.string.url_detection_title, R.string.url_detection_summary) {
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt
index f8a43ca25..06396f1b2 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/AppearanceScreen.kt
@@ -4,7 +4,6 @@ package helium314.keyboard.settings.screens
import android.content.Context
import android.os.Build
import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -35,10 +34,10 @@ import helium314.keyboard.settings.preferences.SwitchPreference
import helium314.keyboard.settings.Theme
import helium314.keyboard.settings.dialogs.ColorThemePickerDialog
import helium314.keyboard.settings.dialogs.CustomizeIconsDialog
-import helium314.keyboard.settings.dialogs.TextInputDialog
import helium314.keyboard.settings.initPreview
import helium314.keyboard.settings.preferences.BackgroundImagePref
import helium314.keyboard.settings.preferences.CustomFontPreference
+import helium314.keyboard.settings.preferences.TextInputPreference
import helium314.keyboard.settings.previewDark
@Composable
@@ -82,6 +81,9 @@ fun AppearanceScreen(
SettingsWithoutKey.CUSTOM_FONT,
Settings.PREF_FONT_SCALE,
Settings.PREF_EMOJI_FONT_SCALE,
+ Settings.PREF_EMOJI_KEY_FIT,
+ if (prefs.getInt(Settings.PREF_EMOJI_MAX_SDK, Defaults.PREF_EMOJI_MAX_SDK) >= 24)
+ Settings.PREF_EMOJI_SKIN_TONE else null,
)
SearchSettingsScreen(
onClickBack = onClickBack,
@@ -223,7 +225,7 @@ fun createAppearanceSettings(context: Context) = listOf(
name = setting.title,
key = setting.key,
default = Defaults.PREF_KEYBOARD_HEIGHT_SCALE,
- range = 0.5f..1.5f,
+ range = 0.3f..1.5f,
description = { "${(100 * it).toInt()}%" }
) { KeyboardSwitcher.getInstance().setThemeNeedsReload() }
},
@@ -263,26 +265,8 @@ fun createAppearanceSettings(context: Context) = listOf(
description = { "${(100 * it).toInt()}%" }
) { KeyboardSwitcher.getInstance().setThemeNeedsReload() }
},
- Setting(context, Settings.PREF_SPACE_BAR_TEXT, R.string.prefs_space_bar_text) { setting ->
- var showDialog by rememberSaveable { mutableStateOf(false) }
- val prefs = LocalContext.current.prefs()
- Preference(
- name = setting.title,
- onClick = { showDialog = true },
- description = prefs.getString(setting.key, Defaults.PREF_SPACE_BAR_TEXT)?.takeIf { it.isNotEmpty() }
- )
- if (showDialog) {
- TextInputDialog(
- onDismissRequest = { showDialog = false },
- onConfirmed = {
- prefs.edit().putString(setting.key, it).apply()
- KeyboardSwitcher.getInstance().setThemeNeedsReload()
- },
- initialText = prefs.getString(setting.key, Defaults.PREF_SPACE_BAR_TEXT) ?: "",
- title = { Text(setting.title) },
- checkTextValid = { true }
- )
- }
+ Setting(context, Settings.PREF_SPACE_BAR_TEXT, R.string.prefs_space_bar_text) {
+ TextInputPreference(it, Defaults.PREF_SPACE_BAR_TEXT)
},
Setting(context, SettingsWithoutKey.CUSTOM_FONT, R.string.custom_font) {
CustomFontPreference(it)
@@ -305,6 +289,20 @@ fun createAppearanceSettings(context: Context) = listOf(
description = { "${(100 * it).toInt()}%" }
) { KeyboardSwitcher.getInstance().setThemeNeedsReload() }
},
+ Setting(context, Settings.PREF_EMOJI_KEY_FIT, R.string.prefs_emoji_key_fit) {
+ SwitchPreference(it, Defaults.PREF_EMOJI_KEY_FIT) { KeyboardSwitcher.getInstance().setThemeNeedsReload() }
+ },
+ Setting(context, Settings.PREF_EMOJI_SKIN_TONE, R.string.prefs_emoji_skin_tone) { setting ->
+ val items = listOf(
+ stringResource(R.string.prefs_emoji_skin_tone_neutral) to "",
+ "\uD83C\uDFFB" to "\uD83C\uDFFB",
+ "\uD83C\uDFFC" to "\uD83C\uDFFC",
+ "\uD83C\uDFFD" to "\uD83C\uDFFD",
+ "\uD83C\uDFFE" to "\uD83C\uDFFE",
+ "\uD83C\uDFFF" to "\uD83C\uDFFF"
+ )
+ ListPreference(setting, items, Defaults.PREF_EMOJI_SKIN_TONE) { KeyboardSwitcher.getInstance().setThemeNeedsReload() }
+ },
)
@Preview
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt
index 5d2a46f12..976a17800 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/ColorsScreen.kt
@@ -58,6 +58,7 @@ import helium314.keyboard.settings.CloseIcon
import helium314.keyboard.settings.SearchScreen
import helium314.keyboard.settings.SettingsActivity
import helium314.keyboard.settings.Theme
+import helium314.keyboard.settings.contentTextDirectionStyle
import helium314.keyboard.settings.dialogs.ColorPickerDialog
import helium314.keyboard.settings.previewDark
import kotlinx.serialization.Serializable
@@ -70,6 +71,14 @@ fun ColorsScreen(
onClickBack: () -> Unit
) {
val ctx = LocalContext.current
+ val prefs = ctx.prefs()
+ val b = (ctx.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
+ if ((b?.value ?: 0) < 0)
+ Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
+
+ val themeName = theme ?: if (isNight) prefs.getString(Settings.PREF_THEME_COLORS_NIGHT, Defaults.PREF_THEME_COLORS_NIGHT)!!
+ else prefs.getString(Settings.PREF_THEME_COLORS, Defaults.PREF_THEME_COLORS)!!
+ var newThemeName by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue(themeName)) }
// is there really no better way of only setting forceOpposite while the screen is shown (and not paused)?
// lifecycle stuff is weird, there is no pause and similar when activity is paused
@@ -82,21 +91,14 @@ fun ColorsScreen(
val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
LaunchedEffect(lifecycleState) {
if (lifecycleState == Lifecycle.State.RESUMED) {
- (ctx.getActivity() as? SettingsActivity)?.setForceTheme(theme, isNight)
+ (ctx.getActivity() as? SettingsActivity)?.setForceTheme(newThemeName.text, isNight)
}
}
- val prefs = ctx.prefs()
- val b = (ctx.getActivity() as? SettingsActivity)?.prefChanged?.collectAsState()
- if ((b?.value ?: 0) < 0)
- Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
-
- val themeName = theme ?: if (isNight) prefs.getString(Settings.PREF_THEME_COLORS_NIGHT, Defaults.PREF_THEME_COLORS_NIGHT)!!
- else prefs.getString(Settings.PREF_THEME_COLORS, Defaults.PREF_THEME_COLORS)!!
- val moreColors = KeyboardTheme.readUserMoreColors(prefs, themeName)
- val userColors = KeyboardTheme.readUserColors(prefs, themeName)
+ val moreColors = KeyboardTheme.readUserMoreColors(prefs, newThemeName.text)
+ val userColors = KeyboardTheme.readUserColors(prefs, newThemeName.text)
val shownColors = if (moreColors == 2) {
- val allColors = KeyboardTheme.readUserAllColors(prefs, themeName)
+ val allColors = KeyboardTheme.readUserAllColors(prefs, newThemeName.text)
ColorType.entries.map {
ColorSetting(it.name, null, allColors[it] ?: it.default())
}
@@ -113,12 +115,11 @@ fun ColorsScreen(
fun ColorSetting.displayColor() = if (auto == true) KeyboardTheme.determineUserColor(userColors, ctx, name, isNight)
else color ?: KeyboardTheme.determineUserColor(userColors, ctx, name, isNight)
- var newThemeName by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue(themeName)) }
var chosenColorString: String by rememberSaveable { mutableStateOf("") }
val chosenColor = runCatching { Json.decodeFromString(chosenColorString) }.getOrNull()
- val saveLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
- val uri = it.data?.data ?: return@rememberLauncherForActivityResult
+ val saveLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
+ val uri = result.data?.data ?: return@rememberLauncherForActivityResult
ctx.getActivity()?.contentResolver?.openOutputStream(uri)?.writer()?.use { it.write(getColorString(prefs, newThemeName.text)) }
}
SearchScreen(
@@ -129,14 +130,17 @@ fun ColorsScreen(
value = nameField,
onValueChange = {
nameValid = KeyboardTheme.renameUserColors(newThemeName.text, it.text, prefs)
- if (nameValid)
+ if (nameValid) {
newThemeName = it
+ SettingsActivity.forceTheme = newThemeName.text
+ }
nameField = it
},
isError = !nameValid,
// supportingText = { if (!nameValid) Text(stringResource(R.string.name_invalid)) } // todo: this is cutting off bottom half of the actual text...
trailingIcon = { if (!nameValid) CloseIcon(R.string.name_invalid) },
singleLine = true,
+ textStyle = contentTextDirectionStyle,
)
},
menu = listOf(
@@ -193,11 +197,11 @@ fun ColorsScreen(
}
}
if (colorSetting.auto != null)
- Switch(colorSetting.auto, onCheckedChange = {
- val oldUserColors = KeyboardTheme.readUserColors(prefs, themeName)
- val newUserColors = (oldUserColors + ColorSetting(colorSetting.name, it, colorSetting.color))
+ Switch(colorSetting.auto, onCheckedChange = { checked ->
+ val oldUserColors = KeyboardTheme.readUserColors(prefs, newThemeName.text)
+ val newUserColors = (oldUserColors + ColorSetting(colorSetting.name, checked, colorSetting.color))
.reversed().distinctBy { it.displayName }
- KeyboardTheme.writeUserColors(prefs, themeName, newUserColors)
+ KeyboardTheme.writeUserColors(prefs, newThemeName.text, newUserColors)
})
}
}
@@ -207,16 +211,16 @@ fun ColorsScreen(
onDismissRequest = { chosenColorString = "" },
initialColor = chosenColor.displayColor(),
title = chosenColor.displayName,
- ) {
+ ) { color ->
if (moreColors == 2) {
- val oldColors = KeyboardTheme.readUserAllColors(prefs, themeName)
- oldColors[ColorType.valueOf(chosenColor.name)] = it
- KeyboardTheme.writeUserAllColors(prefs, themeName, oldColors)
+ val oldColors = KeyboardTheme.readUserAllColors(prefs, newThemeName.text)
+ oldColors[ColorType.valueOf(chosenColor.name)] = color
+ KeyboardTheme.writeUserAllColors(prefs, newThemeName.text, oldColors)
} else {
- val oldUserColors = KeyboardTheme.readUserColors(prefs, themeName)
- val newUserColors = (oldUserColors + ColorSetting(chosenColor.name, false, it))
+ val oldUserColors = KeyboardTheme.readUserColors(prefs, newThemeName.text)
+ val newUserColors = (oldUserColors + ColorSetting(chosenColor.name, false, color))
.reversed().distinctBy { it.displayName }
- KeyboardTheme.writeUserColors(prefs, themeName, newUserColors)
+ KeyboardTheme.writeUserColors(prefs, newThemeName.text, newUserColors)
}
}
}
diff --git a/app/src/main/java/helium314/keyboard/settings/screens/DictionaryScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/DictionaryScreen.kt
index 77d1b9b42..dc64cadd8 100644
--- a/app/src/main/java/helium314/keyboard/settings/screens/DictionaryScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/screens/DictionaryScreen.kt
@@ -55,7 +55,7 @@ fun DictionaryScreen(
) {
val ctx = LocalContext.current
val enabledLanguages = SubtypeSettings.getEnabledSubtypes(true).map { it.locale().language }
- val cachedDictFolders = DictionaryInfoUtils.getCachedDirectoryList(ctx).orEmpty().map { it.name }
+ val cachedDictFolders = DictionaryInfoUtils.getCacheDirectories(ctx).map { it.name }
val comparer = compareBy({ it.language !in enabledLanguages }, { it.toLanguageTag() !in cachedDictFolders}, { it.displayName })
val dictionaryLocales = getDictionaryLocales(ctx).sortedWith(comparer).toMutableList()
dictionaryLocales.add(0, Locale(SubtypeLocaleUtils.NO_LANGUAGE))
@@ -69,7 +69,7 @@ fun DictionaryScreen(
if (term.isBlank()) dictionaryLocales
else dictionaryLocales.filter { loc ->
loc.language != SubtypeLocaleUtils.NO_LANGUAGE
- && loc.localizedDisplayName(ctx).replace("(", "")
+ && loc.localizedDisplayName(ctx.resources).replace("(", "")
.splitOnWhitespace().any { it.startsWith(term, true) }
}
},
@@ -98,7 +98,7 @@ fun DictionaryScreen(
val types = dicts.mapTo(mutableListOf()) { it.name.substringBefore("_${DictionaryInfoUtils.USER_DICTIONARY_SUFFIX}") }
if (hasInternal && !types.contains(Dictionary.TYPE_MAIN))
types.add(0, stringResource(R.string.internal_dictionary_summary))
- Text(locale.localizedDisplayName(ctx))
+ Text(locale.localizedDisplayName(ctx.resources))
Text(
types.joinToString(", "),
style = MaterialTheme.typography.bodyMedium,
@@ -142,8 +142,8 @@ fun DictionaryScreen(
fun getUserAndInternalDictionaries(context: Context, locale: Locale): Pair, Boolean> {
val userDicts = mutableListOf