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..c64158b01
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorImpl.kt
@@ -0,0 +1,825 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+package helium314.keyboard.latin
+
+import android.Manifest
+import android.content.Context
+import android.provider.UserDictionary
+import android.util.LruCache
+import helium314.keyboard.keyboard.Keyboard
+import helium314.keyboard.keyboard.emoji.SupportedEmojis
+import helium314.keyboard.latin.DictionaryFacilitator.DictionaryInitializationListener
+import helium314.keyboard.latin.NgramContext.WordInfo
+import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
+import helium314.keyboard.latin.common.ComposedData
+import helium314.keyboard.latin.common.Constants
+import helium314.keyboard.latin.common.StringUtils
+import helium314.keyboard.latin.common.decapitalize
+import helium314.keyboard.latin.common.splitOnWhitespace
+import helium314.keyboard.latin.permissions.PermissionsUtil
+import helium314.keyboard.latin.personalization.UserHistoryDictionary
+import helium314.keyboard.latin.settings.Settings
+import helium314.keyboard.latin.settings.SettingsValuesForSuggestion
+import helium314.keyboard.latin.utils.Log
+import helium314.keyboard.latin.utils.SubtypeSettings
+import helium314.keyboard.latin.utils.SuggestionResults
+import helium314.keyboard.latin.utils.getSecondaryLocales
+import helium314.keyboard.latin.utils.locale
+import helium314.keyboard.latin.utils.prefs
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.io.File
+import java.io.IOException
+import java.util.Locale
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+/**
+ * Facilitates interaction with different kinds of dictionaries. Provides APIs
+ * to instantiate and select the correct dictionaries (based on language and settings),
+ * update entries and fetch suggestions.
+ *
+ *
+ * Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as
+ * a client for interacting with dictionaries.
+ */
+class DictionaryFacilitatorImpl : DictionaryFacilitator {
+ private var dictionaryGroups = listOf(DictionaryGroup())
+
+ @Volatile
+ private var mLatchForWaitingLoadingMainDictionaries = CountDownLatch(0)
+
+ // The library does not deal well with ngram history for auto-capitalized words, so we adjust
+ // the ngram context to store next word suggestions for such cases.
+ // todo: this is awful, find a better solution / workaround
+ // or remove completely? not sure if it's actually an improvement
+ // should be fixed in the library, but that's not feasible with current user-provides-library approach
+ // added in 12cbd43bda7d0f0cd73925e9cf836de751c32ed0 / https://github.com/Helium314/HeliBoard/issues/135
+ private var tryChangingWords = false
+ private var changeFrom = ""
+ private var changeTo = ""
+
+ // todo: write cache never set, and never read (only written)
+ // tried to use read cache for a while, but small performance improvements are not worth the work,
+ // see https://github.com/Helium314/HeliBoard/issues/307
+ private var mValidSpellingWordReadCache: LruCache? = null
+ private var mValidSpellingWordWriteCache: LruCache? = null
+
+ private val scope = CoroutineScope(Dispatchers.Default)
+
+ override fun setValidSpellingWordReadCache(cache: LruCache) {
+ mValidSpellingWordReadCache = cache
+ }
+
+ override fun setValidSpellingWordWriteCache(cache: LruCache) {
+ mValidSpellingWordWriteCache = cache
+ }
+
+ // judging by usage before adding multilingual typing, this should check primary group locale only
+ override fun isForLocale(locale: Locale?): Boolean {
+ return locale != null && locale == dictionaryGroups[0].locale
+ }
+
+ override fun onStartInput() {
+ }
+
+ override fun onFinishInput(context: Context) {
+ for (dictGroup in dictionaryGroups) {
+ DictionaryFacilitator.ALL_DICTIONARY_TYPES.forEach { dictGroup.getDict(it)?.onFinishInput() }
+ }
+ }
+
+ override fun isActive(): Boolean {
+ return dictionaryGroups[0].locale.language.isNotEmpty()
+ }
+
+ override fun getMainLocale(): Locale {
+ return dictionaryGroups[0].locale
+ }
+
+ override fun getCurrentLocale(): Locale {
+ return currentlyPreferredDictionaryGroup.locale
+ }
+
+ override fun usesSameSettings(locales: List, contacts: Boolean, apps: Boolean, personalization: Boolean): Boolean {
+ val dictGroup = dictionaryGroups[0] // settings are the same for all groups
+ return contacts == dictGroup.hasDict(Dictionary.TYPE_CONTACTS)
+ && apps == dictGroup.hasDict(Dictionary.TYPE_APPS)
+ && personalization == dictGroup.hasDict(Dictionary.TYPE_USER_HISTORY)
+ && locales.size == dictionaryGroups.size
+ && locales.none { findDictionaryGroupWithLocale(dictionaryGroups, it) == null }
+ }
+
+ // -------------- managing (loading & closing) dictionaries ------------
+
+ override fun resetDictionaries(
+ context: Context,
+ newLocale: Locale,
+ useContactsDict: Boolean,
+ useAppsDict: Boolean,
+ usePersonalizedDicts: Boolean,
+ forceReloadMainDictionary: Boolean,
+ dictNamePrefix: String,
+ listener: DictionaryInitializationListener?
+ ) {
+ Log.i(TAG, "resetDictionaries, force reloading main dictionary: $forceReloadMainDictionary")
+
+ val locales = getUsedLocales(newLocale, context)
+
+ val subDictTypesToUse = listOfNotNull(
+ Dictionary.TYPE_USER,
+ if (useAppsDict) Dictionary.TYPE_APPS else null,
+ if (usePersonalizedDicts) Dictionary.TYPE_USER_HISTORY else null,
+ if (useContactsDict && PermissionsUtil.checkAllPermissionsGranted(context, Manifest.permission.READ_CONTACTS))
+ Dictionary.TYPE_CONTACTS else null
+ )
+
+ val (newDictionaryGroups, existingDictsToCleanup) =
+ getNewDictGroupsAndDictsToCleanup(locales, subDictTypesToUse, forceReloadMainDictionary, dictNamePrefix, context)
+
+ // Replace Dictionaries.
+ val oldDictionaryGroups: List
+ synchronized(this) {
+ oldDictionaryGroups = dictionaryGroups
+ dictionaryGroups = newDictionaryGroups
+ if (hasAtLeastOneUninitializedMainDictionary()) {
+ asyncReloadUninitializedMainDictionaries(context, locales, listener)
+ }
+ }
+
+ listener?.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary())
+
+ // Clean up old dictionaries.
+ existingDictsToCleanup.forEach { (locale, dictTypes) ->
+ val dictGroupToCleanup = findDictionaryGroupWithLocale(oldDictionaryGroups, locale) ?: return@forEach
+ for (dictType in dictTypes) {
+ dictGroupToCleanup.closeDict(dictType)
+ }
+ }
+
+ mValidSpellingWordWriteCache?.evictAll()
+ mValidSpellingWordReadCache?.evictAll()
+ }
+
+ /** creates dictionaryGroups for [newLocales] with given [newSubDictTypes], trying to re-use existing dictionaries.
+ * returns the new dictionaryGroups and unused dictionary types by locale */
+ private fun getNewDictGroupsAndDictsToCleanup(
+ newLocales: Collection,
+ newSubDictTypes: Collection,
+ forceReload: Boolean,
+ dictNamePrefix: String,
+ context: Context
+ ): Pair, Map>> {
+ // Gather all dictionaries by locale. We may remove some from the list later.
+ val existingDictsToCleanup = HashMap>()
+ for (dictGroup in dictionaryGroups) {
+ existingDictsToCleanup[dictGroup.locale] = DictionaryFacilitator.ALL_DICTIONARY_TYPES
+ .filterTo(mutableListOf()) { dictGroup.hasDict(it) }
+ }
+
+ // create new dictionary groups and remove dictionaries to re-use from existingDictsToCleanup
+ val newDictionaryGroups = mutableListOf()
+ for (locale in newLocales) {
+ // get existing dictionary group for new locale
+ val oldDictGroupForLocale = findDictionaryGroupWithLocale(dictionaryGroups, locale)
+ val dictTypesToCleanupForLocale = existingDictsToCleanup[locale]
+
+ // create new or re-use already loaded main dict
+ val mainDict: Dictionary?
+ if (forceReload || oldDictGroupForLocale == null
+ || !oldDictGroupForLocale.hasDict(Dictionary.TYPE_MAIN)
+ ) {
+ mainDict = null // null main dicts will be loaded later in asyncReloadUninitializedMainDictionaries
+ } else {
+ mainDict = oldDictGroupForLocale.getDict(Dictionary.TYPE_MAIN)
+ dictTypesToCleanupForLocale?.remove(Dictionary.TYPE_MAIN)
+ }
+
+ // create new or re-use already loaded sub-dicts
+ val subDicts: MutableMap = HashMap()
+ for (subDictType in newSubDictTypes) {
+ val subDict: ExpandableBinaryDictionary
+ if (forceReload || oldDictGroupForLocale == null
+ || !oldDictGroupForLocale.hasDict(subDictType)
+ ) {
+ // Create a new dictionary.
+ subDict = createSubDict(subDictType, context, locale, null, dictNamePrefix) ?: continue
+ } else {
+ // Reuse the existing dictionary.
+ subDict = oldDictGroupForLocale.getSubDict(subDictType) ?: continue
+ dictTypesToCleanupForLocale?.remove(subDictType)
+ }
+ subDicts[subDictType] = subDict
+ }
+ val newDictGroup = DictionaryGroup(locale, mainDict, subDicts, context)
+ newDictionaryGroups.add(newDictGroup)
+ }
+ return newDictionaryGroups to existingDictsToCleanup
+ }
+
+ private fun asyncReloadUninitializedMainDictionaries(
+ context: Context, locales: Collection, listener: DictionaryInitializationListener?
+ ) {
+ val latchForWaitingLoadingMainDictionary = CountDownLatch(1)
+ mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary
+ scope.launch {
+ try {
+ val dictGroupsWithNewMainDict = locales.mapNotNull {
+ val dictionaryGroup = findDictionaryGroupWithLocale(dictionaryGroups, it)
+ if (dictionaryGroup == null) {
+ Log.w(TAG, "Expected a dictionary group for $it but none found")
+ return@mapNotNull null // This should never happen
+ }
+ if (dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.isInitialized == true) null
+ else dictionaryGroup to DictionaryFactory.createMainDictionaryCollection(context, it)
+ }
+ synchronized(this) {
+ dictGroupsWithNewMainDict.forEach { (dictGroup, mainDict) ->
+ dictGroup.setMainDict(mainDict)
+ }
+ }
+
+ listener?.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary())
+ latchForWaitingLoadingMainDictionary.countDown()
+ } catch (e: Throwable) {
+ Log.e(TAG, "could not initialize main dictionaries for $locales", e)
+ }
+ }
+ }
+
+ override fun closeDictionaries() {
+ val dictionaryGroupsToClose: List
+ synchronized(this) {
+ dictionaryGroupsToClose = dictionaryGroups
+ dictionaryGroups = listOf(DictionaryGroup())
+ }
+ for (dictionaryGroup in dictionaryGroupsToClose) {
+ for (dictType in DictionaryFacilitator.ALL_DICTIONARY_TYPES) {
+ dictionaryGroup.closeDict(dictType)
+ }
+ }
+ }
+
+ // The main dictionaries are loaded asynchronously. Don't cache the return value of these methods.
+ override fun hasAtLeastOneInitializedMainDictionary(): Boolean =
+ dictionaryGroups.any { it.getDict(Dictionary.TYPE_MAIN)?.isInitialized == true }
+
+ override fun hasAtLeastOneUninitializedMainDictionary(): Boolean =
+ dictionaryGroups.any { it.getDict(Dictionary.TYPE_MAIN)?.isInitialized != true }
+
+ @Throws(InterruptedException::class)
+ override fun waitForLoadingMainDictionaries(timeout: Long, unit: TimeUnit) {
+ mLatchForWaitingLoadingMainDictionaries.await(timeout, unit)
+ }
+
+ // -------------- actual dictionary stuff like getting suggestions ------------
+
+ override fun addToUserHistory(
+ suggestion: String, wasAutoCapitalized: Boolean, ngramContext: NgramContext,
+ timeStampInSeconds: Long, blockPotentiallyOffensive: Boolean
+ ) {
+ // Update the spelling cache before learning. Words that are not yet added to user history
+ // and appear in no other language model are not considered valid.
+ putWordIntoValidSpellingWordCache("addToUserHistory", suggestion)
+
+ val words = suggestion.splitOnWhitespace().dropLastWhile { it.isEmpty() }
+
+ // increase / decrease confidence
+ if (words.size == 1) // ignore if more than a single word, which only happens with (badly working) spaceAwareGesture
+ adjustConfidences(suggestion, wasAutoCapitalized)
+
+ // Add word to user dictionary if it is in no other dictionary except user history dictionary (i.e. typed again).
+ val sv = Settings.getValues()
+ if (sv.mAddToPersonalDictionary // require the opt-in
+ && sv.mAutoCorrectEnabled == sv.mAutoCorrectionEnabledPerUserSettings // don't add if user wants autocorrect but input field does not, see https://github.com/Helium314/HeliBoard/issues/427#issuecomment-1905438000
+ && dictionaryGroups[0].hasDict(Dictionary.TYPE_USER_HISTORY) // require personalized suggestions
+ && !wasAutoCapitalized // we can't be 100% sure about what the user intended to type, so better don't add it
+ && words.size == 1 // only single words
+ ) {
+ addToPersonalDictionaryIfInvalidButInHistory(suggestion)
+ }
+
+ var ngramContextForCurrentWord = ngramContext
+ val preferredGroup = currentlyPreferredDictionaryGroup
+ for (i in words.indices) {
+ val currentWord = words[i]
+ val wasCurrentWordAutoCapitalized = (i == 0) && wasAutoCapitalized
+ // add to history for preferred dictionary group, to avoid mixing languages in history
+ addWordToUserHistory(
+ preferredGroup, ngramContextForCurrentWord, currentWord,
+ wasCurrentWordAutoCapitalized, timeStampInSeconds.toInt(), blockPotentiallyOffensive
+ )
+ ngramContextForCurrentWord = ngramContextForCurrentWord.getNextNgramContext(WordInfo(currentWord))
+
+ // remove manually entered blacklisted words from blacklist for likely matching languages
+ dictionaryGroups.filter { it.confidence == preferredGroup.confidence }.forEach {
+ it.removeFromBlacklist(currentWord)
+ }
+ }
+ }
+
+ private fun addWordToUserHistory(
+ dictionaryGroup: DictionaryGroup, ngramContext: NgramContext, word: String, wasAutoCapitalized: Boolean,
+ timeStampInSeconds: Int, blockPotentiallyOffensive: Boolean
+ ) {
+ val userHistoryDictionary = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY) ?: return
+
+ val mainFreq = dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.getFrequency(word) ?: Dictionary.NOT_A_PROBABILITY
+ if (mainFreq == 0 && blockPotentiallyOffensive)
+ return
+ if (tryChangingWords)
+ tryChangingWords = ngramContext.changeWordIfAfterBeginningOfSentence(changeFrom, changeTo)
+
+ val wordToUse: String
+ // Check for isBeginningOfSentenceContext too, because not all text fields auto-capitalize in this case.
+ // Even if the user capitalizes manually, they most likely don't want the capitalized form suggested.
+ if (wasAutoCapitalized || ngramContext.isBeginningOfSentenceContext) {
+ val decapitalizedWord = word.decapitalize(dictionaryGroup.locale) // try undoing auto-capitalization
+ if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroup)
+ && !isValidWord(decapitalizedWord, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroup)
+ ) {
+ // If the word was auto-capitalized and exists only as a capitalized word in the
+ // dictionary, then we must not downcase it before registering it. For example,
+ // the name of the contacts in start-of-sentence position would come here with the
+ // wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version
+ // of that contact's name which would end up popping in suggestions.
+ wordToUse = word
+ } else {
+ // If however the word is not in the dictionary, or exists as a de-capitalized word
+ // only, then we consider that was a lower-case word that had been auto-capitalized.
+ wordToUse = decapitalizedWord
+ tryChangingWords = true
+ changeFrom = word
+ changeTo = wordToUse
+ }
+ } else {
+ // HACK: We'd like to avoid adding the capitalized form of common words to the User
+ // History dictionary in order to avoid suggesting them until the dictionary
+ // consolidation is done.
+ // TODO: Remove this hack when ready.
+ val lowerCasedWord = word.lowercase(dictionaryGroup.locale)
+ val lowerCaseFreqInMainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN)?.getFrequency(lowerCasedWord)
+ ?: Dictionary.NOT_A_PROBABILITY
+ wordToUse = if (mainFreq < lowerCaseFreqInMainDict
+ && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT
+ ) {
+ // Use lower cased word as the word can be a distracter of the popular word.
+ lowerCasedWord
+ } else {
+ word
+ }
+ }
+ // We demote unrecognized words (frequency <= 0) by specifying them as "invalid".
+ // We don't add words with 0-frequency (assuming they would be profanity etc.).
+ val isValid = mainFreq > 0
+ UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, wordToUse, isValid, timeStampInSeconds)
+ }
+
+ private fun addToPersonalDictionaryIfInvalidButInHistory(word: String) {
+ if (word.length <= 1) return
+ val dictionaryGroup = clearlyPreferredDictionaryGroup ?: return
+ val userDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER) ?: return
+ val userHistoryDict = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY) ?: return
+ if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroup))
+ return // valid word, no reason to auto-add it to personal dict
+ if (userDict.isInDictionary(word))
+ return // should never happen, but better be safe
+
+ // User history always reports words as invalid, so we check the frequency instead.
+ // Testing shows that after 2 times adding, the frequency is 111, and then rises slowly with usage (values vary slightly).
+ // 120 is after 3 uses of the word, so we simply require more than that. todo: Could be made configurable.
+ // Words added to dictionaries (user and history) seem to be found only after some delay.
+ // This is not too bad, but it delays adding in case a user wants to fill a dictionary using this functionality
+ if (userHistoryDict.getFrequency(word) > 120) {
+ scope.launch {
+ // adding can throw IllegalArgumentException: Unknown URL content://user_dictionary/words
+ // https://stackoverflow.com/q/41474623 https://github.com/AnySoftKeyboard/AnySoftKeyboard/issues/490
+ // apparently some devices don't have a dictionary? or it's just sporadic hiccups?
+ runCatching { UserDictionary.Words.addWord(userDict.mContext, word, 250, null, dictionaryGroup.locale) }
+ }
+ }
+ }
+
+ private fun putWordIntoValidSpellingWordCache(caller: String, originalWord: String) {
+ if (mValidSpellingWordWriteCache == null)
+ return
+
+ val lowerCaseWord = originalWord.lowercase(currentLocale)
+ val lowerCaseValid = isValidSpellingWord(lowerCaseWord)
+ mValidSpellingWordWriteCache?.put(lowerCaseWord, lowerCaseValid)
+
+ val capitalWord = StringUtils.capitalizeFirstAndDowncaseRest(originalWord, currentLocale)
+ val capitalValid = if (lowerCaseValid) {
+ true // The lower case form of the word is valid, so the upper case must be valid.
+ } else {
+ isValidSpellingWord(capitalWord)
+ }
+ mValidSpellingWordWriteCache?.put(capitalWord, capitalValid)
+ }
+
+ override fun adjustConfidences(word: String, wasAutoCapitalized: Boolean) {
+ if (dictionaryGroups.size == 1 || word.contains(Constants.WORD_SEPARATOR))
+ return
+
+ // if suggestion was auto-capitalized, check against both the suggestion and the de-capitalized suggestion
+ val decapitalizedSuggestion = if (wasAutoCapitalized) word.decapitalize(currentLocale) else word
+ dictionaryGroups.forEach {
+ if (isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it)) {
+ it.increaseConfidence()
+ return@forEach
+ }
+ // also increase confidence if suggestion was auto-capitalized and the lowercase variant it valid
+ if (wasAutoCapitalized && isValidWord(decapitalizedSuggestion, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it))
+ it.increaseConfidence()
+ else it.decreaseConfidence()
+ }
+ }
+
+ /** the dictionaryGroup with most confidence, first group when tied */
+ private val currentlyPreferredDictionaryGroup: DictionaryGroup get() = dictionaryGroups.maxBy { it.confidence }
+
+ /** the only dictionary group, or the dictionaryGroup confidence >= DictionaryGroup.MAX_CONFIDENCE if all others have 0 */
+ private val clearlyPreferredDictionaryGroup: DictionaryGroup? get() {
+ if (dictionaryGroups.size == 1) return dictionaryGroups.first() // confidence not used if we only have a single group
+
+ val preferred = currentlyPreferredDictionaryGroup
+ if (preferred.confidence < DictionaryGroup.MAX_CONFIDENCE) return null
+ if (dictionaryGroups.any { it.confidence > 0 && it !== preferred })
+ return null
+ return preferred
+ }
+
+ override fun unlearnFromUserHistory(word: String, ngramContext: NgramContext, timeStampInSeconds: Long, eventType: Int) {
+ // TODO: Decide whether or not to remove the word on EVENT_BACKSPACE.
+ if (eventType != Constants.EVENT_BACKSPACE) {
+ currentlyPreferredDictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY)?.removeUnigramEntryDynamically(word)
+ }
+
+ // Update the spelling cache after unlearning. Words that are removed from user history
+ // and appear in no other language model are not considered valid.
+ putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.lowercase(Locale.getDefault()))
+ }
+
+ // TODO: Revise the way to fusion suggestion results.
+ override fun getSuggestionResults(
+ composedData: ComposedData, ngramContext: NgramContext, keyboard: Keyboard,
+ settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int, inputStyle: Int
+ ): SuggestionResults {
+ val proximityInfoHandle = keyboard.proximityInfo.nativeProximityInfo
+ val weightOfLangModelVsSpatialModel = floatArrayOf(Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL)
+
+ val waitForOtherDicts = if (dictionaryGroups.size == 1) null else CountDownLatch(dictionaryGroups.size - 1)
+ val suggestionsArray = Array?>(dictionaryGroups.size) { null }
+ for (i in 1..dictionaryGroups.lastIndex) {
+ scope.launch {
+ suggestionsArray[i] = getSuggestions(composedData, ngramContext, settingsValuesForSuggestion, sessionId,
+ proximityInfoHandle, weightOfLangModelVsSpatialModel, dictionaryGroups[i])
+ waitForOtherDicts?.countDown()
+ }
+ }
+ suggestionsArray[0] = getSuggestions(composedData, ngramContext, settingsValuesForSuggestion, sessionId,
+ proximityInfoHandle, weightOfLangModelVsSpatialModel, dictionaryGroups[0])
+ val suggestionResults = SuggestionResults(
+ SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext, false
+ )
+ waitForOtherDicts?.await()
+
+ suggestionsArray.forEach {
+ if (it == null) return@forEach
+ suggestionResults.addAll(it)
+ suggestionResults.mRawSuggestions?.addAll(it)
+ }
+
+ return suggestionResults
+ }
+
+ private fun getSuggestions(
+ composedData: ComposedData, ngramContext: NgramContext,
+ settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int,
+ proximityInfoHandle: Long, weightOfLangModelVsSpatialModel: FloatArray, dictGroup: DictionaryGroup
+ ): List {
+ val suggestions = ArrayList()
+ val weightForLocale = dictGroup.getWeightForLocale(dictionaryGroups, composedData.mIsBatchMode)
+ for (dictType in DictionaryFacilitator.ALL_DICTIONARY_TYPES) {
+ val dictionary = dictGroup.getDict(dictType) ?: continue
+ val dictionarySuggestions = dictionary.getSuggestions(composedData, ngramContext, proximityInfoHandle,
+ settingsValuesForSuggestion, sessionId, weightForLocale, weightOfLangModelVsSpatialModel
+ ) ?: continue
+
+ // For some reason "garbage" words are produced when glide typing. For user history
+ // and main dictionaries we can filter them out by checking whether the dictionary
+ // actually contains the word. But personal and addon dictionaries may contain shortcuts,
+ // which do not pass an isInDictionary check (e.g. emojis).
+ // (if the main dict contains shortcuts to non-words, this will break!)
+ val checkForGarbage = composedData.mIsBatchMode && (dictType == Dictionary.TYPE_USER_HISTORY || dictType == Dictionary.TYPE_MAIN)
+
+ for (info in dictionarySuggestions) {
+ val word = info.word
+ if (isBlacklisted(word) || SupportedEmojis.isUnsupported(word)) // don't add blacklisted words and unsupported emojis
+ continue
+ if (checkForGarbage
+ // consider the user might use custom main dictionary containing shortcuts
+ // assume this is unlikely to happen, and take care about common shortcuts that are not actual words (emoji, symbols)
+ && word.length > 2 // should exclude most symbol shortcuts
+ && info.mSourceDict.mDictType == dictType // dictType is always main, but info.mSourceDict.mDictType contains the actual dict (main dict is a dictionary group)
+ && !StringUtils.mightBeEmoji(word) // simplified check for performance reasons
+ && !dictionary.isInDictionary(word)
+ )
+ continue
+
+ if (word.length == 1 && info.mSourceDict.mDictType == "emoji" && !StringUtils.mightBeEmoji(word[0].code))
+ continue
+
+ suggestions.add(info)
+ }
+ }
+ return suggestions
+ }
+
+ // Spell checker is using this, and has its own instance of DictionaryFacilitatorImpl,
+ // meaning that it always has default mConfidence. So we cannot choose to only check preferred
+ // locale, and instead simply return true if word is in any of the available dictionaries
+ override fun isValidSpellingWord(word: String): Boolean {
+ mValidSpellingWordReadCache?.get(word)?.let { return it }
+ val result = dictionaryGroups.any { isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, it) }
+ mValidSpellingWordReadCache?.put(word, result)
+ return result
+ }
+
+ // this is unused, so leave it for now (redirecting to isValidWord seems to defeat the purpose...)
+ override fun isValidSuggestionWord(word: String): Boolean {
+ return isValidWord(word, DictionaryFacilitator.ALL_DICTIONARY_TYPES, dictionaryGroups[0])
+ }
+
+ // todo: move into dictionaryGroup?
+ private fun isValidWord(word: String, dictionariesToCheck: Array, dictionaryGroup: DictionaryGroup): Boolean {
+ if (word.isEmpty() || dictionaryGroup.isBlacklisted(word)) return false
+ return dictionariesToCheck.any { dictionaryGroup.getDict(it)?.isValidWord(word) == true }
+ }
+
+ private fun isBlacklisted(word: String): Boolean = dictionaryGroups.any { it.isBlacklisted(word) }
+
+ override fun removeWord(word: String) {
+ for (dictionaryGroup in dictionaryGroups) {
+ dictionaryGroup.removeWord(word)
+ }
+ }
+
+ override fun clearUserHistoryDictionary(context: Context) {
+ for (dictionaryGroup in dictionaryGroups) {
+ dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY)?.clear()
+ }
+ }
+
+ override fun localesAndConfidences(): String? {
+ if (dictionaryGroups.size < 2) return null
+ return dictionaryGroups.joinToString(", ") { "${it.locale} ${it.confidence}" }
+ }
+
+ override fun dumpDictionaryForDebug(dictName: String) {
+ val dictToDump = dictionaryGroups[0].getSubDict(dictName)
+ if (dictToDump == null) {
+ Log.e(TAG, ("Cannot dump $dictName. The dictionary is not being used for suggestion or cannot be dumped."))
+ return
+ }
+ dictToDump.dumpAllWordsForDebug()
+ }
+
+ override fun getDictionaryStats(context: Context): List =
+ DictionaryFacilitator.DYNAMIC_DICTIONARY_TYPES.flatMap { dictType ->
+ dictionaryGroups.mapNotNull { it.getSubDict(dictType)?.dictionaryStats }
+ }
+
+ override fun dump(context: Context) = getDictionaryStats(context).joinToString("\n")
+
+ companion object {
+ private val TAG = DictionaryFacilitatorImpl::class.java.simpleName
+
+ // HACK: This threshold is being used when adding a capitalized entry in the User History dictionary.
+ private const val CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140
+
+ private fun createSubDict(
+ dictType: String, context: Context, locale: Locale, dictFile: File?, dictNamePrefix: String
+ ): ExpandableBinaryDictionary? {
+ try {
+ return when (dictType) {
+ Dictionary.TYPE_USER_HISTORY -> UserHistoryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
+ Dictionary.TYPE_USER -> UserBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
+ Dictionary.TYPE_CONTACTS -> ContactsBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
+ Dictionary.TYPE_APPS -> AppsBinaryDictionary.getDictionary(context, locale, dictFile, dictNamePrefix)
+ else -> throw IllegalArgumentException("unknown dictionary type $dictType")
+ }
+ } catch (e: SecurityException) {
+ Log.e(TAG, "Cannot create dictionary: $dictType", e)
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "Cannot create dictionary: $dictType", e)
+ }
+ return null
+ }
+
+ private fun findDictionaryGroupWithLocale(dictGroups: List?, locale: Locale): DictionaryGroup? {
+ return dictGroups?.firstOrNull { it.locale == locale }
+ }
+
+ private fun getUsedLocales(mainLocale: Locale, context: Context): Collection {
+ val locales = hashSetOf(mainLocale)
+ // adding secondary locales is a bit tricky since they depend on the subtype
+ // but usually this is called with the selected subtype locale
+ val selectedSubtype = SubtypeSettings.getSelectedSubtype(context.prefs())
+ if (selectedSubtype.locale() == mainLocale) {
+ locales.addAll(getSecondaryLocales(selectedSubtype.extraValue))
+ } else {
+ // probably we're called from the spell checker when using a different app as keyboard
+ // so best bet is adding all secondary locales for matching main locale
+ SubtypeSettings.getEnabledSubtypes(false).forEach {
+ if (it.locale() == mainLocale)
+ locales.addAll(getSecondaryLocales(it.extraValue))
+ }
+ }
+ return locales
+ }
+ }
+}
+
+/** A group of dictionaries that work together for a single language. */
+private class DictionaryGroup(
+ val locale: Locale = Locale(""),
+ private var mainDict: Dictionary? = null,
+ subDicts: Map = emptyMap(),
+ context: Context? = null
+) {
+ private val subDicts: ConcurrentHashMap = ConcurrentHashMap(subDicts)
+
+ /** Removes a word from all dictionaries in this group. If the word is in a read-only dictionary, it is blacklisted. */
+ fun removeWord(word: String) {
+ // remove from user history
+ getSubDict(Dictionary.TYPE_USER_HISTORY)?.removeUnigramEntryDynamically(word)
+
+ // and from personal dictionary
+ getSubDict(Dictionary.TYPE_USER)?.removeUnigramEntryDynamically(word)
+
+ val contactsDict = getSubDict(Dictionary.TYPE_CONTACTS)
+ if (contactsDict != null && contactsDict.isInDictionary(word)) {
+ contactsDict.removeUnigramEntryDynamically(word) // will be gone until next reload of dict
+ addToBlacklist(word)
+ return
+ }
+
+ val appsDict = getSubDict(Dictionary.TYPE_APPS)
+ if (appsDict != null && appsDict.isInDictionary(word)) {
+ appsDict.removeUnigramEntryDynamically(word) // will be gone until next reload of dict
+ addToBlacklist(word)
+ return
+ }
+
+ val mainDict = mainDict ?: return
+ if (mainDict.isValidWord(word)) {
+ addToBlacklist(word)
+ return
+ }
+
+ val lowercase = word.lowercase(locale)
+ if (getDict(Dictionary.TYPE_MAIN)!!.isValidWord(lowercase)) {
+ addToBlacklist(lowercase)
+ }
+ }
+
+ // --------------- Confidence for multilingual typing -------------------
+
+ // Confidence that the most probable language is actually the language the user is
+ // typing in. For now, this is simply the number of times a word from this language
+ // has been committed in a row, with an exception when typing a single word not contained
+ // in this language.
+ var confidence = 1
+
+ // allow to go above max confidence, for better determination of currently preferred language
+ // when decreasing confidence or getting weight factor, limit to maximum
+ fun increaseConfidence() {
+ confidence += 1
+ }
+
+ // If confidence is above max, drop to max confidence. This does not change weights and
+ // allows conveniently typing single words from the other language without affecting suggestions
+ fun decreaseConfidence() {
+ if (confidence > MAX_CONFIDENCE) confidence = MAX_CONFIDENCE
+ else if (confidence > 0) {
+ confidence -= 1
+ }
+ }
+
+ fun getWeightForLocale(groups: List, isGesturing: Boolean) =
+ getWeightForLocale(groups, if (isGesturing) 0.05f else 0.15f)
+
+ // might need some more tuning
+ fun getWeightForLocale(groups: List, step: Float): Float {
+ if (groups.size == 1) return 1f
+ if (confidence < 2) return 1f - step * (MAX_CONFIDENCE - confidence)
+ for (group in groups) {
+ if (group !== this && group.confidence >= confidence) return 1f - step / 2f
+ }
+ return 1f
+ }
+
+ // --------------- Blacklist -------------------
+
+ private val scope = CoroutineScope(Dispatchers.IO)
+
+ // words cannot be (permanently) removed from some dictionaries, so we use a blacklist for "removing" words
+ private val blacklistFile = if (context?.filesDir == null) null
+ else {
+ val file = File(context.filesDir.absolutePath + File.separator + "blacklists" + File.separator + locale.toLanguageTag() + ".txt")
+ if (file.isDirectory) file.delete() // this apparently was an issue in some versions
+ if (file.parentFile?.exists() == true || file.parentFile?.mkdirs() == true) file
+ else null
+ }
+
+ private val blacklist = hashSetOf().apply {
+ if (blacklistFile?.isFile != true) return@apply
+ scope.launch {
+ synchronized(this) {
+ try {
+ addAll(blacklistFile.readLines())
+ } catch (e: IOException) {
+ Log.e(TAG, "Exception while trying to read blacklist from ${blacklistFile.name}", e)
+ }
+ }
+ }
+ }
+
+ fun isBlacklisted(word: String) = blacklist.contains(word)
+
+ fun addToBlacklist(word: String) {
+ if (!blacklist.add(word) || blacklistFile == null) return
+ scope.launch {
+ synchronized(this) {
+ try {
+ if (blacklistFile.isDirectory) blacklistFile.delete()
+ blacklistFile.appendText("$word\n")
+ } catch (e: IOException) {
+ Log.e(TAG, "Exception while trying to add word \"$word\" to blacklist ${blacklistFile.name}", e)
+ }
+ }
+ }
+ }
+
+ fun removeFromBlacklist(word: String) {
+ if (!blacklist.remove(word) || blacklistFile == null) return
+ scope.launch {
+ synchronized(this) {
+ try {
+ val newLines = blacklistFile.readLines().filterNot { it == word }
+ blacklistFile.writeText(newLines.joinToString("\n"))
+ } catch (e: IOException) {
+ Log.e(TAG, "Exception while trying to remove word \"$word\" to blacklist ${blacklistFile.name}", e)
+ }
+ }
+ }
+ }
+
+ // --------------- Dictionary handling -------------------
+
+ fun setMainDict(newMainDict: Dictionary?) {
+ // Close old dictionary if exists. Main dictionary can be assigned multiple times.
+ val oldDict = mainDict
+ mainDict = newMainDict
+ if (oldDict != null && newMainDict !== oldDict)
+ oldDict.close()
+ }
+
+ fun getDict(dictType: String): Dictionary? {
+ if (dictType == Dictionary.TYPE_MAIN) {
+ return mainDict
+ }
+ return getSubDict(dictType)
+ }
+
+ fun getSubDict(dictType: String): ExpandableBinaryDictionary? {
+ return subDicts[dictType]
+ }
+
+ fun hasDict(dictType: String): Boolean {
+ if (dictType == Dictionary.TYPE_MAIN) {
+ return mainDict != null
+ }
+ return subDicts.containsKey(dictType)
+ }
+
+ fun closeDict(dictType: String) {
+ val dict = if (Dictionary.TYPE_MAIN == dictType) {
+ mainDict
+ } else {
+ subDicts.remove(dictType)
+ }
+ dict?.close()
+ }
+
+ companion object {
+ private val TAG = DictionaryGroup::class.java.simpleName
+ const val MAX_CONFIDENCE = 2
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorLruCache.java b/app/src/main/java/helium314/keyboard/latin/DictionaryFacilitatorLruCache.java
index 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..576bd90a4 100644
--- a/app/src/main/java/helium314/keyboard/latin/DictionaryFactory.kt
+++ b/app/src/main/java/helium314/keyboard/latin/DictionaryFactory.kt
@@ -6,88 +6,102 @@
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)
+ }
+ 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) {
+ val dictionary = getDictionary(file, locale) ?: return
+ if (dicts.any { it.mDictType == dictionary.mDictType }) {
+ dictionary.close()
+ return
+ }
+ dicts.add(dictionary)
+ }
+
+ @JvmStatic
+ fun getDictionary(
+ file: File,
+ locale: Locale
+ ): Dictionary? {
+ if (!file.isFile) return null
+ val header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file)
+ if (header == null) {
+ killDictionary(file)
+ return null
+ }
+ val dictType = header.mIdString.split(":").first()
+ val readOnlyBinaryDictionary = ReadOnlyBinaryDictionary(
+ file.absolutePath, 0, file.length(), false, locale, dictType
+ )
+
+ if (readOnlyBinaryDictionary.isValidDictionary) {
+ if (locale.language == "ko") {
+ // Use KoreanDictionary for Korean locale
+ return KoreanDictionary(readOnlyBinaryDictionary)
+ }
+ return readOnlyBinaryDictionary
}
- } else {
readOnlyBinaryDictionary.close()
killDictionary(file)
+ return null
+ }
+
+ private fun killDictionary(file: File) {
+ Log.e("DictionaryFactory", "could not load dictionary ${file.parentFile?.name}/${file.name}, deleting")
+ file.delete()
}
}
-
-private fun killDictionary(file: File) {
- Log.e("DictionaryFactory", "could not load dictionary ${file.parentFile?.name}/${file.name}, deleting")
- file.delete()
-}
diff --git a/app/src/main/java/helium314/keyboard/latin/EmojiAltPhysicalKeyDetector.java b/app/src/main/java/helium314/keyboard/latin/EmojiAltPhysicalKeyDetector.java
index 79bfeeb24..9b06420ee 100644
--- a/app/src/main/java/helium314/keyboard/latin/EmojiAltPhysicalKeyDetector.java
+++ b/app/src/main/java/helium314/keyboard/latin/EmojiAltPhysicalKeyDetector.java
@@ -23,7 +23,7 @@ import java.util.List;
/**
* A class for detecting Emoji-Alt physical key.
*/
-final class EmojiAltPhysicalKeyDetector {
+public final class EmojiAltPhysicalKeyDetector {
private static final String TAG = "EmojiAltPhysKeyDetector";
private static final boolean DEBUG = false;
diff --git a/app/src/main/java/helium314/keyboard/latin/InputAttributes.java b/app/src/main/java/helium314/keyboard/latin/InputAttributes.java
index 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..ab5998348 100644
--- a/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt
+++ b/app/src/main/java/helium314/keyboard/latin/KeyboardWrapperView.kt
@@ -78,12 +78,13 @@ 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 landscape = Settings.getValues().mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE
+ val split = Settings.getValues().mIsSplitKeyboardEnabled
+ val oldScale = Settings.readOneHandedModeScale(context.prefs(), landscape, split)
val newScale = (oldScale + changePercent / 100f).coerceAtMost(2.5f).coerceAtLeast(0.5f)
if (newScale == oldScale) return@setOnTouchListener true
Settings.getInstance().writeOneHandedModeScale(newScale)
- oneHandedModeEnabled = false // intentionally putting wrong value, so KeyboardSwitcher.setOneHandedModeEnabled does actually reload
- KeyboardSwitcher.getInstance().setOneHandedModeEnabled(true)
+ KeyboardSwitcher.getInstance().setOneHandedModeEnabled(true, true)
}
else -> x = 0f
}
diff --git a/app/src/main/java/helium314/keyboard/latin/KoreanDictionary.java b/app/src/main/java/helium314/keyboard/latin/KoreanDictionary.java
index 9b791148c..2b6722777 100644
--- a/app/src/main/java/helium314/keyboard/latin/KoreanDictionary.java
+++ b/app/src/main/java/helium314/keyboard/latin/KoreanDictionary.java
@@ -4,6 +4,7 @@ package helium314.keyboard.latin;
import helium314.keyboard.event.HangulCombiner;
import helium314.keyboard.latin.common.ComposedData;
+import helium314.keyboard.latin.makedict.WordProperty;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
import java.text.Normalizer;
@@ -72,6 +73,11 @@ public class KoreanDictionary extends Dictionary {
return mDictionary.getMaxFrequencyOfExactMatches(processInput(word));
}
+ @Override
+ public WordProperty getWordProperty(String word, boolean isBeginningOfSentence) {
+ return mDictionary.getWordProperty(processInput(word), isBeginningOfSentence);
+ }
+
@Override
protected boolean same(char[] word, int length, String typedWord) {
word = processInput(new String(word)).toCharArray();
diff --git a/app/src/main/java/helium314/keyboard/latin/LatinIME.java b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
index 5913bf701..9e2ed3c72 100644
--- a/app/src/main/java/helium314/keyboard/latin/LatinIME.java
+++ b/app/src/main/java/helium314/keyboard/latin/LatinIME.java
@@ -26,7 +26,6 @@ import android.os.Process;
import android.text.InputType;
import android.util.PrintWriterPrinter;
import android.util.Printer;
-import android.util.SparseArray;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
@@ -51,16 +50,12 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.common.InsetsOutlineProvider;
import helium314.keyboard.dictionarypack.DictionaryPackConstants;
import helium314.keyboard.event.Event;
-import helium314.keyboard.event.HangulEventDecoder;
-import helium314.keyboard.event.HardwareEventDecoder;
-import helium314.keyboard.event.HardwareKeyboardEventDecoder;
import helium314.keyboard.event.InputTransaction;
import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.keyboard.KeyboardId;
import helium314.keyboard.keyboard.KeyboardLayoutSet;
import helium314.keyboard.keyboard.KeyboardSwitcher;
import helium314.keyboard.keyboard.MainKeyboardView;
-import helium314.keyboard.latin.Suggest.OnGetSuggestedWordsCallback;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ColorType;
import helium314.keyboard.latin.common.Constants;
@@ -69,7 +64,6 @@ import helium314.keyboard.latin.common.InputPointers;
import helium314.keyboard.latin.common.LocaleUtils;
import helium314.keyboard.latin.common.ViewOutlineProviderUtilsKt;
import helium314.keyboard.latin.define.DebugFlags;
-import helium314.keyboard.latin.define.ProductionFlags;
import helium314.keyboard.latin.inputlogic.InputLogic;
import helium314.keyboard.latin.personalization.PersonalizationHelper;
import helium314.keyboard.latin.settings.Settings;
@@ -87,6 +81,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;
@@ -128,12 +123,12 @@ public class LatinIME extends InputMethodService implements
public final KeyboardActionListener mKeyboardActionListener;
private int mOriginalNavBarColor = 0;
private int mOriginalNavBarFlags = 0;
+
+ // UIHandler is needed when creating InputLogic
+ public final UIHandler mHandler = new UIHandler(this);
private final DictionaryFacilitator mDictionaryFacilitator =
DictionaryFacilitatorProvider.getDictionaryFacilitator(false);
final InputLogic mInputLogic = new InputLogic(this, this, mDictionaryFacilitator);
- // We expect to have only one decoder in almost all cases, hence the default capacity of 1.
- // If it turns out we need several, it will get grown seamlessly.
- final SparseArray mHardwareEventDecoders = new SparseArray<>(1);
// TODO: Move these {@link View}s to {@link KeyboardSwitcher}.
private View mInputView;
@@ -143,7 +138,6 @@ public class LatinIME extends InputMethodService implements
private RichInputMethodManager mRichImm;
final KeyboardSwitcher mKeyboardSwitcher;
private final SubtypeState mSubtypeState = new SubtypeState();
- private EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector;
private final StatsUtilsManager mStatsUtilsManager;
// Working variable for {@link #startShowingInputView()} and
// {@link #onEvaluateInputViewShown()}.
@@ -186,8 +180,6 @@ public class LatinIME extends InputMethodService implements
private final ClipboardHistoryManager mClipboardHistoryManager = new ClipboardHistoryManager(this);
- public final UIHandler mHandler = new UIHandler(this);
-
public static final class UIHandler extends LeakGuardHandlerWrapper {
private static final int MSG_UPDATE_SHIFT_STATE = 0;
private static final int MSG_PENDING_IMS_CALLBACK = 1;
@@ -278,9 +270,7 @@ public class LatinIME extends InputMethodService implements
msg.arg2 /* remainingTries */, this /* handler */)) {
// If we were able to reset the caches, then we can reload the keyboard.
// Otherwise, we'll do it when we can.
- latinIme.mKeyboardSwitcher.loadKeyboard(latinIme.getCurrentInputEditorInfo(),
- settingsValues, latinIme.getCurrentAutoCapsState(),
- latinIme.getCurrentRecapitalizeState());
+ latinIme.mKeyboardSwitcher.reloadMainKeyboard();
}
break;
case MSG_WAIT_FOR_DICTIONARY_LOAD:
@@ -608,7 +598,7 @@ public class LatinIME extends InputMethodService implements
mCurrentSubtypeHasBeenUsed = false;
}
if (currentSubtypeHasBeenUsed
- && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype)
+ && SubtypeSettings.INSTANCE.isEnabled(lastActiveSubtype)
&& !currentSubtype.equals(lastActiveSubtype)) {
switchToSubtype(lastActiveSubtype);
return;
@@ -636,6 +626,7 @@ public class LatinIME extends InputMethodService implements
@Override
public void onCreate() {
+ mSettings.startListener();
KeyboardIconsSet.Companion.getInstance().loadIcons(this);
mRichImm = RichInputMethodManager.getInstance();
AudioAndHapticFeedbackManager.init(this);
@@ -663,7 +654,8 @@ public class LatinIME extends InputMethodService implements
final IntentFilter newDictFilter = new IntentFilter();
newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
- ContextCompat.registerReceiver(this, mDictionaryPackInstallReceiver, newDictFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
+ // RECEIVER_EXPORTED is necessary because apparently Android 15 (and others?) don't recognize if the sender and receiver are the same app, see https://github.com/Helium314/HeliBoard/pull/1756
+ ContextCompat.registerReceiver(this, mDictionaryPackInstallReceiver, newDictFilter, ContextCompat.RECEIVER_EXPORTED);
final IntentFilter dictDumpFilter = new IntentFilter();
dictDumpFilter.addAction(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION);
@@ -737,8 +729,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;
}
@@ -754,9 +746,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);
}
@@ -765,12 +762,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
@@ -793,14 +787,6 @@ public class LatinIME extends InputMethodService implements
deallocateMemory();
}
- public void recycle() {
- unregisterReceiver(mDictionaryPackInstallReceiver);
- unregisterReceiver(mDictionaryDumpBroadcastReceiver);
- unregisterReceiver(mRingerModeChangeReceiver);
- unregisterReceiver(mRestartAfterDeviceUnlockReceiver);
- mInputLogic.recycle();
- }
-
private boolean isImeSuppressedByHardwareKeyboard() {
final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance();
return !onEvaluateInputViewShown() && switcher.isImeSuppressedByHardwareKeyboard(
@@ -882,7 +868,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);
}
@@ -934,8 +921,9 @@ public class LatinIME extends InputMethodService implements
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 */
@@ -1023,6 +1011,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
@@ -1072,7 +1062,7 @@ public class LatinIME extends InputMethodService implements
if (isDifferentTextField) {
mainKeyboardView.closing();
suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold);
- switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(), getCurrentRecapitalizeState());
+ switcher.reloadMainKeyboard();
if (needToCallLoadKeyboardLater) {
// If we need to call loadKeyboard again later, we need to save its state now. The
// later call will be done in #retryResetCaches.
@@ -1120,6 +1110,7 @@ public class LatinIME extends InputMethodService implements
@Override
public void onWindowHidden() {
super.onWindowHidden();
+ Log.i(TAG, "onWindowHidden");
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
if (mainKeyboardView != null) {
mainKeyboardView.closing();
@@ -1176,8 +1167,12 @@ public class LatinIME extends InputMethodService implements
if (isInputViewShown()
&& mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
composingSpanStart, composingSpanEnd, settingsValues)) {
- mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(),
- getCurrentRecapitalizeState());
+ // we don't want to update a manually set shift state if selection changed towards one side
+ // because this may end the manual shift, which is unwanted in case of shift + arrow keys for changing selection
+ // todo: this is not fully implemented yet, and maybe should be behind a setting
+ if (mKeyboardSwitcher.getKeyboard() != null && mKeyboardSwitcher.getKeyboard().mId.isAlphabetShiftedManually()
+ && !((oldSelEnd == newSelEnd && oldSelStart != newSelStart) || (oldSelEnd != newSelEnd && oldSelStart == newSelStart)))
+ mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState());
}
}
@@ -1218,7 +1213,8 @@ public class LatinIME extends InputMethodService implements
@Override
public void hideWindow() {
- if (mSuggestionStripView != null)
+ Log.i(TAG, "hideWindow");
+ if (hasSuggestionStripView() && mSettings.getCurrent().mToolbarMode == ToolbarMode.EXPANDABLE)
mSuggestionStripView.setToolbarVisibility(false);
mKeyboardSwitcher.onHideWindow();
@@ -1230,6 +1226,12 @@ public class LatinIME extends InputMethodService implements
super.hideWindow();
}
+ @Override
+ public void requestHideSelf(int flags) {
+ super.requestHideSelf(flags);
+ Log.i(TAG, "requestHideSelf: " + flags);
+ }
+
@Override
public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
if (DebugFlags.DEBUG_ENABLED) {
@@ -1274,20 +1276,25 @@ 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();
if (isImeSuppressedByHardwareKeyboard() && !visibleKeyboardView.isShown()) {
// If there is a hardware keyboard and a visible software keyboard view has been hidden,
// no visual element will be shown on the screen.
- outInsets.contentTopInsets = inputHeight;
- outInsets.visibleTopInsets = inputHeight;
+ // for some reason setting contentTopInsets and visibleTopInsets broke somewhere along the
+ // way from OpenBoard to HeliBoard (GH-702, GH-1455), but not setting anything seems to work
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;
@@ -1371,6 +1378,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);
}
@@ -1378,6 +1389,10 @@ public class LatinIME extends InputMethodService implements
@RequiresApi(api = Build.VERSION_CODES.R)
public boolean onInlineSuggestionsResponse(InlineSuggestionsResponse response) {
Log.d(TAG,"onInlineSuggestionsResponse called");
+ if (Settings.getValues().mSuggestionStripHiddenPerUserSettings) {
+ return false;
+ }
+
final List inlineSuggestions = response.getInlineSuggestions();
if (inlineSuggestions.isEmpty()) {
return false;
@@ -1464,7 +1479,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)
@@ -1527,25 +1542,7 @@ public class LatinIME extends InputMethodService implements
// Implementation of {@link SuggestionStripView.Listener}.
@Override
public void onCodeInput(final int codePoint, final int x, final int y, final boolean isKeyRepeat) {
- onCodeInput(codePoint, 0, x, y, isKeyRepeat);
- }
-
- public void onCodeInput(final int codePoint, final int metaState, final int x, final int y, final boolean isKeyRepeat) {
- if (codePoint < 0) {
- switch (codePoint) {
- case KeyCode.TOGGLE_AUTOCORRECT -> {mSettings.toggleAutoCorrect(); return; }
- case KeyCode.TOGGLE_INCOGNITO_MODE -> {mSettings.toggleAlwaysIncognitoMode(); return; }
- }
- }
- final Event event;
- // checking if the character is a combining accent
- if (0x300 <= codePoint && codePoint <= 0x35b) {
- event = Event.createSoftwareDeadEvent(codePoint, 0, metaState, x, y, null);
- } else {
- event = createSoftwareKeypressEvent(codePoint, metaState, x, y, isKeyRepeat);
- }
-
- onEvent(event);
+ mKeyboardActionListener.onCodeInput(codePoint, x, y, isKeyRepeat);
}
// This method is public for testability of LatinIME, but also in the future it should
@@ -1562,24 +1559,6 @@ public class LatinIME extends InputMethodService implements
mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState());
}
- // A helper method to split the code point and the key code. Ultimately, they should not be
- // squashed into the same variable, and this method should be removed.
- // public for testing, as we don't want to copy the same logic into test code
- @NonNull
- public static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int metaState,
- final int keyX, final int keyY, final boolean isKeyRepeat) {
- final int keyCode;
- final int codePoint;
- if (keyCodeOrCodePoint <= 0) {
- keyCode = keyCodeOrCodePoint;
- codePoint = Event.NOT_A_CODE_POINT;
- } else {
- keyCode = Event.NOT_A_KEY_CODE;
- codePoint = keyCodeOrCodePoint;
- }
- return Event.createSoftwareKeypressEvent(codePoint, keyCode, metaState, keyX, keyY, isKeyRepeat);
- }
-
public void onTextInput(final String rawText) {
// TODO: have the keyboard pass the correct key code when we need it.
final Event event = Event.createSoftwareTextEvent(rawText, KeyCode.MULTIPLE_CODE_POINTS);
@@ -1632,7 +1611,7 @@ public class LatinIME extends InputMethodService implements
dismissGestureFloatingPreviewText /* dismissDelayed */);
}
- public boolean hasSuggestionStripView() {
+ private boolean hasSuggestionStripView() {
return null != mSuggestionStripView;
}
@@ -1667,18 +1646,6 @@ public class LatinIME extends InputMethodService implements
}
}
- // TODO[IL]: Move this out of LatinIME.
- public void getSuggestedWords(final int inputStyle, final int sequenceNumber,
- final OnGetSuggestedWordsCallback callback) {
- final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
- if (keyboard == null) {
- callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance());
- return;
- }
- mInputLogic.getSuggestedWords(mSettings.getCurrent(), keyboard,
- mKeyboardSwitcher.getKeyboardShiftMode(), inputStyle, sequenceNumber, callback);
- }
-
@Override
public void showSuggestionStrip(final SuggestedWords suggestedWords) {
if (suggestedWords.isEmpty()) {
@@ -1763,8 +1730,7 @@ public class LatinIME extends InputMethodService implements
loadSettings();
if (mKeyboardSwitcher.getMainKeyboardView() != null) {
// Reload keyboard because the current language has been changed.
- mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent(),
- getCurrentAutoCapsState(), getCurrentRecapitalizeState());
+ mKeyboardSwitcher.reloadMainKeyboard();
}
}
@@ -1832,63 +1798,18 @@ public class LatinIME extends InputMethodService implements
feedbackManager.performAudioFeedback(code);
}
- private HardwareEventDecoder getHardwareKeyEventDecoder(final int deviceId) {
- final HardwareEventDecoder decoder = mHardwareEventDecoders.get(deviceId);
- if (null != decoder) return decoder;
- // TODO: create the decoder according to the specification
- final HardwareEventDecoder newDecoder = new HardwareKeyboardEventDecoder(deviceId);
- mHardwareEventDecoders.put(deviceId, newDecoder);
- return newDecoder;
- }
-
// Hooks for hardware keyboard
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
- if (mEmojiAltPhysicalKeyDetector == null) {
- mEmojiAltPhysicalKeyDetector = new EmojiAltPhysicalKeyDetector(
- getApplicationContext().getResources());
- }
- mEmojiAltPhysicalKeyDetector.onKeyDown(keyEvent);
- if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
- return super.onKeyDown(keyCode, keyEvent);
- }
- final Event event;
- if (mRichImm.getCurrentSubtypeLocale().getLanguage().equals("ko")) {
- final RichInputMethodSubtype subtype = mKeyboardSwitcher.getKeyboard() == null
- ? mRichImm.getCurrentSubtype()
- : mKeyboardSwitcher.getKeyboard().mId.mSubtype;
- event = HangulEventDecoder.decodeHardwareKeyEvent(subtype, keyEvent,
- () -> getHardwareKeyEventDecoder(keyEvent.getDeviceId()).decodeHardwareKey(keyEvent));
- } else {
- event = getHardwareKeyEventDecoder(keyEvent.getDeviceId()).decodeHardwareKey(keyEvent);
- }
- // If the event is not handled by LatinIME, we just pass it to the parent implementation.
- // If it's handled, we return true because we did handle it.
- if (event.isHandled()) {
- mInputLogic.onCodeInput(mSettings.getCurrent(), event,
- mKeyboardSwitcher.getKeyboardShiftMode(),
- // TODO: this is not necessarily correct for a hardware keyboard right now
- mKeyboardSwitcher.getCurrentKeyboardScript(),
- mHandler);
+ if (mKeyboardActionListener.onKeyDown(keyCode, keyEvent))
return true;
- }
return super.onKeyDown(keyCode, keyEvent);
}
@Override
public boolean onKeyUp(final int keyCode, final KeyEvent keyEvent) {
- if (mEmojiAltPhysicalKeyDetector == null) {
- mEmojiAltPhysicalKeyDetector = new EmojiAltPhysicalKeyDetector(
- getApplicationContext().getResources());
- }
- mEmojiAltPhysicalKeyDetector.onKeyUp(keyEvent);
- if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
- return super.onKeyUp(keyCode, keyEvent);
- }
- final long keyIdentifier = (long) keyEvent.getDeviceId() << 32 + keyEvent.getKeyCode();
- if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
+ if (mKeyboardActionListener.onKeyUp(keyCode, keyEvent))
return true;
- }
return super.onKeyUp(keyCode, keyEvent);
}
@@ -2021,8 +1942,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/ReadOnlyBinaryDictionary.java b/app/src/main/java/helium314/keyboard/latin/ReadOnlyBinaryDictionary.java
index b4cfe5f3a..f757e87eb 100644
--- a/app/src/main/java/helium314/keyboard/latin/ReadOnlyBinaryDictionary.java
+++ b/app/src/main/java/helium314/keyboard/latin/ReadOnlyBinaryDictionary.java
@@ -10,6 +10,7 @@ import com.android.inputmethod.latin.BinaryDictionary;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
+import helium314.keyboard.latin.makedict.WordProperty;
import helium314.keyboard.latin.settings.SettingsValuesForSuggestion;
import java.util.ArrayList;
@@ -107,6 +108,18 @@ public final class ReadOnlyBinaryDictionary extends Dictionary {
return NOT_A_PROBABILITY;
}
+ @Override
+ public WordProperty getWordProperty(String word, boolean isBeginningOfSentence) {
+ if (mLock.readLock().tryLock()) {
+ try {
+ return mBinaryDictionary.getWordProperty(word, isBeginningOfSentence);
+ } finally {
+ mLock.readLock().unlock();
+ }
+ }
+ return null;
+ }
+
@Override
public void close() {
mLock.writeLock().lock();
diff --git a/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java b/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java
index 3d81249b1..f7e8bcc03 100644
--- a/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java
+++ b/app/src/main/java/helium314/keyboard/latin/RichInputConnection.java
@@ -40,8 +40,6 @@ import helium314.keyboard.latin.settings.SpacingAndPunctuations;
import helium314.keyboard.latin.utils.CapsModeUtils;
import helium314.keyboard.latin.utils.DebugLogUtils;
import helium314.keyboard.latin.utils.NgramContextUtils;
-import helium314.keyboard.latin.utils.ScriptUtils;
-import helium314.keyboard.latin.utils.SpannableStringUtils;
import helium314.keyboard.latin.utils.StatsUtils;
import helium314.keyboard.latin.utils.TextRange;
@@ -441,7 +439,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
// test for this explicitly)
if (INVALID_CURSOR_POSITION != mExpectedSelStart
&& (cachedLength >= n || cachedLength >= mExpectedSelStart)) {
- final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText);
+ final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText.toString());
// We call #toString() here to create a temporary object.
// In some situations, this method is called on a worker thread, and it's possible
// the main thread touches the contents of mComposingText while this worker thread
@@ -718,8 +716,13 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (start < 0 || end < 0) {
return false;
}
- mExpectedSelStart = start;
- mExpectedSelEnd = end;
+ if (start > end) {
+ mExpectedSelStart = end;
+ mExpectedSelEnd = start;
+ } else {
+ mExpectedSelStart = start;
+ mExpectedSelEnd = end;
+ }
if (isConnected()) {
final boolean isIcValid = mIC.setSelection(start, end);
if (!isIcValid) {
@@ -825,15 +828,6 @@ public final class RichInputConnection implements PrivateCommandPerformer {
return NgramContextUtils.getNgramContextFromNthPreviousWord(prev, spacingAndPunctuations, n);
}
- private static boolean isPartOfCompositionForScript(final int codePoint,
- final SpacingAndPunctuations spacingAndPunctuations, final String script) {
- // We always consider word connectors part of compositions.
- return spacingAndPunctuations.isWordConnector(codePoint)
- // Otherwise, it's part of composition if it's part of script and not a separator.
- || (!spacingAndPunctuations.isWordSeparator(codePoint)
- && ScriptUtils.isLetterPartOfScript(codePoint, script));
- }
-
/**
* Returns the text surrounding the cursor.
*
@@ -860,90 +854,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (before == null || after == null) {
return null;
}
-
- // Going backward, find the first breaking point (separator)
- int startIndexInBefore = before.length();
- int endIndexInAfter = -1;
- while (startIndexInBefore > 0) {
- final int codePoint = Character.codePointBefore(before, startIndexInBefore);
- if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
- if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
- break;
- // continue to the next whitespace and see whether this contains a sometimesWordConnector
- for (int i = startIndexInBefore - 1; i >= 0; i--) {
- final char c = before.charAt(i);
- if (spacingAndPunctuations.isSometimesWordConnector(c)) {
- // if yes -> whitespace is the index
- startIndexInBefore = Math.max(StringUtils.charIndexOfLastWhitespace(before), 0);
- final int firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after);
- endIndexInAfter = firstSpaceAfter == -1 ? after.length() : firstSpaceAfter -1;
- break;
- } else if (Character.isWhitespace(c)) {
- // if no, just break normally
- break;
- }
- }
- break;
- }
- --startIndexInBefore;
- if (Character.isSupplementaryCodePoint(codePoint)) {
- --startIndexInBefore;
- }
- }
-
- // Find last word separator after the cursor
- if (endIndexInAfter == -1) {
- while (++endIndexInAfter < after.length()) {
- final int codePoint = Character.codePointAt(after, endIndexInAfter);
- if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
- if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
- break;
- // continue to the next whitespace and see whether this contains a sometimesWordConnector
- for (int i = endIndexInAfter; i < after.length(); i++) {
- final char c = after.charAt(i);
- if (spacingAndPunctuations.isSometimesWordConnector(c)) {
- // if yes -> whitespace is next to the index
- startIndexInBefore = Math.max(StringUtils.charIndexOfLastWhitespace(before), 0);
- final int firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after);
- endIndexInAfter = firstSpaceAfter == -1 ? after.length() : firstSpaceAfter - 1;
- break;
- } else if (Character.isWhitespace(c)) {
- // if no, just break normally
- break;
- }
- }
- break;
- }
- if (Character.isSupplementaryCodePoint(codePoint)) {
- ++endIndexInAfter;
- }
- }
- }
-
- // strip stuff before "//" (i.e. ignore http and other protocols)
- final String beforeConsideringStart = before.subSequence(startIndexInBefore, before.length()).toString();
- final int protocolEnd = beforeConsideringStart.lastIndexOf("//");
- if (protocolEnd != -1)
- startIndexInBefore += protocolEnd + 1;
-
- // we don't want the end characters to be word separators
- while (endIndexInAfter > 0 && spacingAndPunctuations.isWordSeparator(after.charAt(endIndexInAfter - 1))) {
- --endIndexInAfter;
- }
- while (startIndexInBefore < before.length() && spacingAndPunctuations.isWordSeparator(before.charAt(startIndexInBefore))) {
- ++startIndexInBefore;
- }
-
- final boolean hasUrlSpans =
- SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length())
- || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter);
- // We don't use TextUtils#concat because it copies all spans without respect to their
- // nature. If the text includes a PARAGRAPH span and it has been split, then
- // TextUtils#concat will crash when it tries to concat both sides of it.
- return new TextRange(
- SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after),
- startIndexInBefore, before.length() + endIndexInAfter, before.length(),
- hasUrlSpans);
+ return StringUtilsKt.getTouchedWordRange(before, after, script, spacingAndPunctuations);
}
public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations,
@@ -956,19 +867,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
// a composing region should always count as a word
return true;
}
- final String textBeforeCursor = mCommittedTextBeforeComposingText.toString();
- int indexOfCodePointInJavaChars = textBeforeCursor.length();
- int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
- : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
- // Search for the first non word-connector char
- if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) {
- indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint);
- consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
- : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
- }
- return !(Constants.NOT_A_CODE == consideredCodePoint
- || spacingAndPunctuations.isWordSeparator(consideredCodePoint)
- || spacingAndPunctuations.isWordConnector(consideredCodePoint));
+ return StringUtilsKt.endsWithWordCodepoint(mCommittedTextBeforeComposingText.toString(), spacingAndPunctuations);
}
public boolean isCursorFollowedByWordCharacter(
diff --git a/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java b/app/src/main/java/helium314/keyboard/latin/RichInputMethodManager.java
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..2e16586b5
--- /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..4e2d62486
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/SingleDictionaryFacilitator.kt
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: GPL-3.0-only
+package helium314.keyboard.latin
+
+import android.content.Context
+import android.util.LruCache
+import helium314.keyboard.keyboard.Keyboard
+import helium314.keyboard.keyboard.KeyboardSwitcher
+import helium314.keyboard.latin.DictionaryFacilitator.DictionaryInitializationListener
+import helium314.keyboard.latin.common.ComposedData
+import helium314.keyboard.latin.makedict.WordProperty
+import helium314.keyboard.latin.settings.SettingsValuesForSuggestion
+import helium314.keyboard.latin.utils.SuggestionResults
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+
+/** Simple DictionaryFacilitator for a single Dictionary. Has some optional special purpose functionality. */
+class SingleDictionaryFacilitator(private val dict: Dictionary) : DictionaryFacilitator {
+ var suggestionLogger: SuggestionLogger? = null
+
+ // this will not work from spell checker if used together with a different keyboard app
+ fun getSuggestions(word: String): SuggestionResults {
+ val suggestionResults = getSuggestionResults(
+ ComposedData.createForWord(word),
+ NgramContext.getEmptyPrevWordsContext(0),
+ KeyboardSwitcher.getInstance().keyboard, // looks like actual keyboard doesn't matter (composed data doesn't contain coordinates)
+ SettingsValuesForSuggestion(false, false),
+ Suggest.SESSION_ID_TYPING, SuggestedWords.INPUT_STYLE_TYPING
+ )
+ return suggestionResults
+ }
+
+ override fun getSuggestionResults(
+ composedData: ComposedData, ngramContext: NgramContext, keyboard: Keyboard,
+ settingsValuesForSuggestion: SettingsValuesForSuggestion, sessionId: Int, inputStyle: Int
+ ): SuggestionResults {
+ val suggestionResults = SuggestionResults(
+ SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext,
+ false
+ )
+ suggestionResults.addAll(
+ dict.getSuggestions(composedData, ngramContext, keyboard.proximityInfo.nativeProximityInfo,
+ settingsValuesForSuggestion, sessionId, 1f,
+ floatArrayOf(Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL)
+ )
+ )
+ suggestionLogger?.onNewSuggestions(suggestionResults, composedData, ngramContext, keyboard, inputStyle)
+
+ return suggestionResults
+ }
+
+ fun getWordProperty(word: String): WordProperty? = dict.getWordProperty(word, false)
+
+ // ------------ dummy functionality ----------------
+
+ override fun setValidSpellingWordReadCache(cache: LruCache) {}
+
+ override fun setValidSpellingWordWriteCache(cache: LruCache) {}
+
+ override fun isForLocale(locale: Locale?): Boolean = locale == dict.mLocale
+
+ override fun onStartInput() {}
+
+ override fun onFinishInput(context: Context) {
+ dict.onFinishInput()
+ }
+
+ override fun closeDictionaries() {
+ dict.close()
+ }
+
+ override fun isActive(): Boolean = true
+
+ override fun getMainLocale(): Locale = dict.mLocale
+
+ override fun getCurrentLocale(): Locale = mainLocale
+
+ override fun usesSameSettings(locales: List, contacts: Boolean, apps: Boolean, personalization: Boolean): Boolean {
+ return locales.singleOrNull() == mainLocale
+ }
+
+ override fun resetDictionaries(context: Context, newLocale: Locale, useContactsDict: Boolean, useAppsDict: Boolean,
+ usePersonalizedDicts: Boolean, forceReloadMainDictionary: Boolean, dictNamePrefix: String, listener: DictionaryInitializationListener?
+ ) { }
+
+ override fun hasAtLeastOneInitializedMainDictionary(): Boolean = dict.isInitialized
+
+ override fun hasAtLeastOneUninitializedMainDictionary(): Boolean = !dict.isInitialized
+
+ override fun waitForLoadingMainDictionaries(timeout: Long, unit: TimeUnit) {
+ }
+
+ override fun addToUserHistory(
+ suggestion: String, wasAutoCapitalized: Boolean, ngramContext: NgramContext,
+ timeStampInSeconds: Long, blockPotentiallyOffensive: Boolean
+ ) {}
+
+ override fun adjustConfidences(word: String, wasAutoCapitalized: Boolean) {}
+
+ override fun unlearnFromUserHistory(word: String, ngramContext: NgramContext, timeStampInSeconds: Long, eventType: Int) {}
+
+ override fun isValidSpellingWord(word: String): Boolean = dict.isValidWord(word)
+
+ override fun isValidSuggestionWord(word: String) = isValidSpellingWord(word)
+
+ override fun removeWord(word: String) {}
+
+ override fun clearUserHistoryDictionary(context: Context) {}
+
+ override fun localesAndConfidences(): String? = null
+
+ override fun dumpDictionaryForDebug(dictName: String) {}
+
+ override fun getDictionaryStats(context: Context): List = emptyList()
+
+ override fun dump(context: Context) = getDictionaryStats(context).joinToString("\n")
+
+ companion object {
+ interface SuggestionLogger {
+ /** provides input data and suggestions returned by the library */
+ fun onNewSuggestions(suggestions: SuggestionResults, composedData: ComposedData,
+ ngramContext: NgramContext, keyboard: Keyboard, inputStyle: Int)
+ }
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/Suggest.kt b/app/src/main/java/helium314/keyboard/latin/Suggest.kt
index 3172acc6a..7e95cf6e2 100644
--- a/app/src/main/java/helium314/keyboard/latin/Suggest.kt
+++ b/app/src/main/java/helium314/keyboard/latin/Suggest.kt
@@ -45,19 +45,20 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
mAutoCorrectionThreshold = threshold
}
+ // todo: remove when InputLogic is ready
interface OnGetSuggestedWordsCallback {
fun onGetSuggestedWords(suggestedWords: SuggestedWords?)
}
fun getSuggestedWords(wordComposer: WordComposer, ngramContext: NgramContext, keyboard: Keyboard,
settingsValuesForSuggestion: SettingsValuesForSuggestion, isCorrectionEnabled: Boolean,
- inputStyle: Int, sequenceNumber: Int, callback: OnGetSuggestedWordsCallback) {
- if (wordComposer.isBatchMode) {
+ inputStyle: Int, sequenceNumber: Int): SuggestedWords {
+ return if (wordComposer.isBatchMode) {
getSuggestedWordsForBatchInput(wordComposer, ngramContext, keyboard, settingsValuesForSuggestion,
- inputStyle, sequenceNumber, callback)
+ inputStyle, sequenceNumber)
} else {
getSuggestedWordsForNonBatchInput(wordComposer, ngramContext, keyboard, settingsValuesForSuggestion,
- inputStyle, isCorrectionEnabled, sequenceNumber, callback)
+ inputStyle, isCorrectionEnabled, sequenceNumber)
}
}
@@ -65,7 +66,7 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
// and calls the callback function with the suggestions.
private fun getSuggestedWordsForNonBatchInput(wordComposer: WordComposer, ngramContext: NgramContext, keyboard: Keyboard,
settingsValuesForSuggestion: SettingsValuesForSuggestion, inputStyleIfNotPrediction: Int,
- isCorrectionEnabled: Boolean, sequenceNumber: Int, callback: OnGetSuggestedWordsCallback) {
+ isCorrectionEnabled: Boolean, sequenceNumber: Int): SuggestedWords {
val typedWordString = wordComposer.typedWord
val resultsArePredictions = !wordComposer.isComposingWord
val suggestionResults = if (typedWordString.isEmpty())
@@ -131,8 +132,8 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
}
}
val isTypedWordValid = firstOccurrenceOfTypedWordInSuggestions > -1 || (!resultsArePredictions && !allowsToBeAutoCorrected)
- callback.onGetSuggestedWords(SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions,
- typedWordInfo, isTypedWordValid, hasAutoCorrection, false, inputStyle, sequenceNumber))
+ return SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions,
+ typedWordInfo, isTypedWordValid, hasAutoCorrection, false, inputStyle, sequenceNumber)
}
// returns [allowsToBeAutoCorrected, hasAutoCorrection]
@@ -252,9 +253,8 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
wordComposer: WordComposer,
ngramContext: NgramContext, keyboard: Keyboard,
settingsValuesForSuggestion: SettingsValuesForSuggestion,
- inputStyle: Int, sequenceNumber: Int,
- callback: OnGetSuggestedWordsCallback
- ) {
+ inputStyle: Int, sequenceNumber: Int
+ ): SuggestedWords {
val suggestionResults = mDictionaryFacilitator.getSuggestionResults(
wordComposer.composedDataSnapshot, ngramContext, keyboard,
settingsValuesForSuggestion, SESSION_ID_GESTURE, inputStyle
@@ -312,10 +312,8 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) {
} else {
suggestionsContainer
}
- callback.onGetSuggestedWords(
- SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions, pseudoTypedWordInfo, true,
- false, false, inputStyle, sequenceNumber)
- )
+ return SuggestedWords(suggestionsList, suggestionResults.mRawSuggestions, pseudoTypedWordInfo, true,
+ false, false, inputStyle, sequenceNumber)
}
/** reduces score of the first suggestion if next one is close and has more than a single letter */
diff --git a/app/src/main/java/helium314/keyboard/latin/UserBinaryDictionary.java b/app/src/main/java/helium314/keyboard/latin/UserBinaryDictionary.java
index 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 203eb205c..6c84ada65 100644
--- a/app/src/main/java/helium314/keyboard/latin/WordComposer.java
+++ b/app/src/main/java/helium314/keyboard/latin/WordComposer.java
@@ -10,8 +10,6 @@ import androidx.annotation.NonNull;
import helium314.keyboard.event.CombinerChain;
import helium314.keyboard.event.Event;
-import helium314.keyboard.keyboard.Keyboard;
-import helium314.keyboard.keyboard.KeyboardSwitcher;
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode;
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo;
import helium314.keyboard.latin.common.ComposedData;
@@ -73,7 +71,7 @@ public final class WordComposer {
private boolean mIsOnlyFirstCharCapitalized;
public WordComposer() {
- mCombinerChain = new CombinerChain("");
+ mCombinerChain = new CombinerChain("", "");
mEvents = new ArrayList<>();
mAutoCorrection = null;
mIsResumed = false;
@@ -81,11 +79,6 @@ public final class WordComposer {
mCursorPositionWithinWord = 0;
mRejectedBatchModeSuggestion = null;
refreshTypedWordCache();
- final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard();
- if (keyboard != null)
- // initializing with the right state is important for the spell checker,
- // which creates a new WordComposer when receiving suggestions
- mCombinerChain.setHangul(keyboard.mId.mSubtype.getLocale().getLanguage().equals("ko"));
}
public ComposedData getComposedDataSnapshot() {
@@ -99,14 +92,11 @@ public final class WordComposer {
public void restartCombining(final String combiningSpec) {
final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec;
if (!nonNullCombiningSpec.equals(mCombiningSpec)) {
- mCombinerChain = new CombinerChain(mCombinerChain.getComposingWordWithCombiningFeedback().toString());
+ mCombinerChain = new CombinerChain(mCombinerChain.getComposingWordWithCombiningFeedback().toString(), nonNullCombiningSpec);
mCombiningSpec = nonNullCombiningSpec;
}
}
- /** Forwards the state to CombinerChain, which disables or enables the Hangul combiner */
- public void setHangul(final boolean enabled) { mCombinerChain.setHangul(enabled); }
-
/**
* Clear out the keys registered so far.
*/
diff --git a/app/src/main/java/helium314/keyboard/latin/common/Colors.kt b/app/src/main/java/helium314/keyboard/latin/common/Colors.kt
index f8a57a476..708122828 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/Colors.kt
+++ b/app/src/main/java/helium314/keyboard/latin/common/Colors.kt
@@ -278,7 +278,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override
TOOL_BAR_EXPAND_KEY_BACKGROUND -> if (!isNight) accent else doubleAdjustedBackground
GESTURE_TRAIL -> gesture
KEY_TEXT, SUGGESTION_AUTO_CORRECT, REMOVE_SUGGESTION_ICON, EMOJI_KEY_TEXT, KEY_PREVIEW_TEXT, POPUP_KEY_TEXT,
- KEY_ICON, ONE_HANDED_MODE_BUTTON, EMOJI_CATEGORY, TOOL_BAR_KEY, FUNCTIONAL_KEY_TEXT -> keyText
+ 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
@@ -476,7 +476,7 @@ class DefaultColors (
TOOL_BAR_EXPAND_KEY_BACKGROUND, CLIPBOARD_SUGGESTION_BACKGROUND -> doubleAdjustedBackground
GESTURE_TRAIL -> gesture
KEY_TEXT, REMOVE_SUGGESTION_ICON, FUNCTIONAL_KEY_TEXT, KEY_ICON, EMOJI_KEY_TEXT,
- POPUP_KEY_TEXT, KEY_PREVIEW_TEXT -> keyText
+ POPUP_KEY_TEXT, POPUP_KEY_ICON, KEY_PREVIEW_TEXT -> keyText
KEY_HINT_TEXT -> keyHintText
SPACE_BAR_TEXT -> spaceBarText
FUNCTIONAL_KEY_BACKGROUND -> functionalKey
@@ -637,6 +637,7 @@ enum class ColorType {
MORE_SUGGESTIONS_WORD_BACKGROUND,
POPUP_KEYS_BACKGROUND,
POPUP_KEY_TEXT,
+ POPUP_KEY_ICON,
NAVIGATION_BAR,
SHIFT_KEY_ICON,
SPACE_BAR_BACKGROUND,
diff --git a/app/src/main/java/helium314/keyboard/latin/common/ComposedData.java b/app/src/main/java/helium314/keyboard/latin/common/ComposedData.java
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..b61586bb2 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/Constants.java
+++ b/app/src/main/java/helium314/keyboard/latin/common/Constants.java
@@ -77,7 +77,7 @@ public final class Constants {
/** Indicates that this subtype is an additional subtype that the user defined. This extra value is private to LatinIME. */
public static final String IS_ADDITIONAL_SUBTYPE = "isAdditionalSubtype";
- /** The subtype extra value used to specify the combining rules (currently not used). */
+ /** The subtype extra value used to specify the combining rules. */
public static final String COMBINING_RULES = "CombiningRules";
/** Overrides the general popup order setting */
@@ -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/Constants.kt b/app/src/main/java/helium314/keyboard/latin/common/Constants.kt
index 17b36d1da..43b9e66dd 100644
--- a/app/src/main/java/helium314/keyboard/latin/common/Constants.kt
+++ b/app/src/main/java/helium314/keyboard/latin/common/Constants.kt
@@ -12,3 +12,5 @@ object Links {
const val CUSTOM_LAYOUTS = "$GITHUB/discussions/categories/custom-layout"
const val CUSTOM_COLORS = "$GITHUB/discussions/categories/custom-colors"
}
+
+val combiningRange = 0x300..0x35b
diff --git a/app/src/main/java/helium314/keyboard/latin/common/LocaleUtils.kt b/app/src/main/java/helium314/keyboard/latin/common/LocaleUtils.kt
index 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..5a501aaad 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,72 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.common.StringUtils.mightBeEmoji
import helium314.keyboard.latin.common.StringUtils.newSingleCodePointString
import helium314.keyboard.latin.settings.SpacingAndPunctuations
+import helium314.keyboard.latin.utils.ScriptUtils
+import helium314.keyboard.latin.utils.SpacedTokens
+import helium314.keyboard.latin.utils.SpannableStringUtils
+import helium314.keyboard.latin.utils.TextRange
import java.math.BigInteger
import java.util.Locale
+import kotlin.math.max
-fun 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)
+
+/** Loops over the codepoints in [text]. Exits when [loop] returns true */
+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()
+/** Loops backwards over the codepoints in [text]. Exits when [loop] returns true */
+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,21 +81,124 @@ 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 s.substring(offset)
+}
+
+/**
+ * Returns whether the [text] does not end with word separator, ignoring all word connectors.
+ * If the [text] is empty (after ignoring word connectors), the method returns false.
+ */
+// todo: this returns true on numbers, why isn't Character.isLetter(code) used?
+fun endsWithWordCodepoint(text: String, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
+ if (text.isEmpty()) return false
+ var codePoint = 0 // initial value irrelevant since length is always > 0
+ loopOverCodePointsBackwards(text) { cp, _ ->
+ codePoint = cp
+ !spacingAndPunctuations.isWordConnector(cp)
+ }
+ // codePoint might still be a wordConnector (if text consists of wordConnectors)
+ return !spacingAndPunctuations.isWordConnector(codePoint) && !spacingAndPunctuations.isWordSeparator(codePoint)
+}
+
+// todo: simplify... maybe compare with original code?
+fun getTouchedWordRange(before: CharSequence, after: CharSequence, script: String, spacingAndPunctuations: SpacingAndPunctuations): TextRange {
+ // Going backward, find the first breaking point (separator)
+ var startIndexInBefore = before.length
+ var endIndexInAfter = -1 // todo: clarify why might we want to set it when checking before
+ loopOverCodePointsBackwards(before) { codePoint, cpLength ->
+ if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
+ if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
+ return@loopOverCodePointsBackwards true
+ // continue to the next whitespace and see whether this contains a sometimesWordConnector
+ for (i in startIndexInBefore - 1 downTo 0) {
+ val c = before[i]
+ if (spacingAndPunctuations.isSometimesWordConnector(c.code)) {
+ // if yes -> whitespace is the index
+ startIndexInBefore = max(StringUtils.charIndexOfLastWhitespace(before).toDouble(), 0.0).toInt()
+ val firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after)
+ endIndexInAfter = if (firstSpaceAfter == -1) after.length else firstSpaceAfter - 1
+ return@loopOverCodePointsBackwards true
+ } else if (Character.isWhitespace(c)) {
+ // if no, just break normally
+ return@loopOverCodePointsBackwards true
+ }
+ }
+ return@loopOverCodePointsBackwards true
+ }
+ startIndexInBefore -= cpLength
+ false
+ }
+
+ // Find last word separator after the cursor
+ if (endIndexInAfter == -1) {
+ endIndexInAfter = 0
+ loopOverCodePoints(after) { codePoint, cpLength ->
+ if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, script)) {
+ if (Character.isWhitespace(codePoint) || !spacingAndPunctuations.mCurrentLanguageHasSpaces)
+ return@loopOverCodePoints true
+ // continue to the next whitespace and see whether this contains a sometimesWordConnector
+ for (i in endIndexInAfter.. whitespace is next to the index
+ startIndexInBefore = max(StringUtils.charIndexOfLastWhitespace(before), 0)
+ val firstSpaceAfter = StringUtils.charIndexOfFirstWhitespace(after)
+ endIndexInAfter = if (firstSpaceAfter == -1) after.length else firstSpaceAfter - 1
+ return@loopOverCodePoints true
+ } else if (Character.isWhitespace(c)) {
+ // if no, just break normally
+ return@loopOverCodePoints true
+ }
+ }
+ return@loopOverCodePoints true
+ }
+ endIndexInAfter += cpLength
+ false
}
}
- return text.substring(offset)
+
+ // strip text before "//" (i.e. ignore http and other protocols)
+ val beforeConsideringStart = before.substring(startIndexInBefore, before.length)
+ val protocolEnd = beforeConsideringStart.lastIndexOf("//")
+ if (protocolEnd != -1) startIndexInBefore += protocolEnd + 1
+
+ // we don't want the end characters to be word separators
+ while (endIndexInAfter > 0 && spacingAndPunctuations.isWordSeparator(after[endIndexInAfter - 1].code)) {
+ --endIndexInAfter
+ }
+ while (startIndexInBefore < before.length && spacingAndPunctuations.isWordSeparator(before[startIndexInBefore].code)) {
+ ++startIndexInBefore
+ }
+
+ val hasUrlSpans = SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length)
+ || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter)
+
+ // We don't use TextUtils#concat because it copies all spans without respect to their
+ // nature. If the text includes a PARAGRAPH span and it has been split, then
+ // TextUtils#concat will crash when it tries to concat both sides of it.
+ return TextRange(
+ SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after),
+ startIndexInBefore, before.length + endIndexInAfter, before.length,
+ hasUrlSpans
+ )
}
+// actually this should not be in STRING Utils, but only used for getTouchedWordRange
+private fun isPartOfCompositionForScript(codePoint: Int, spacingAndPunctuations: SpacingAndPunctuations, script: String) =
+ spacingAndPunctuations.isWordConnector(codePoint) // We always consider word connectors part of compositions.
+ // Otherwise, it's part of composition if it's part of script and not a separator.
+ || (!spacingAndPunctuations.isWordSeparator(codePoint) && ScriptUtils.isLetterPartOfScript(codePoint, script))
+
/** split the string on the first of consecutive space only, further consecutive spaces are added to the next split */
fun String.splitOnFirstSpacesOnly(): List {
val out = mutableListOf()
@@ -110,8 +218,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 +227,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 +242,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/define/ProductionFlags.kt b/app/src/main/java/helium314/keyboard/latin/define/ProductionFlags.kt
index d476a0426..9aeb7cc3d 100644
--- a/app/src/main/java/helium314/keyboard/latin/define/ProductionFlags.kt
+++ b/app/src/main/java/helium314/keyboard/latin/define/ProductionFlags.kt
@@ -7,15 +7,11 @@
package helium314.keyboard.latin.define
object ProductionFlags {
- const val IS_HARDWARE_KEYBOARD_SUPPORTED = false
- // todo: make it work
- // was set to true in hangul branch (and there is the hangul hardware event decoder in latinIme)
- // but disabled again because this breaks ctrl+c / ctrl+v, and most likely other things
- // so it looks like the HardwareKeyboardEventDecoder needs some work before it's ready
+ const val IS_HARDWARE_KEYBOARD_SUPPORTED = true
/**
* Include all suggestions from all dictionaries in
* [helium314.keyboard.latin.SuggestedWords.mRawSuggestions].
*/
const val INCLUDE_RAW_SUGGESTIONS = false
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
index 79464dee3..28572fa2f 100644
--- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
+++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java
@@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo;
import androidx.annotation.NonNull;
import helium314.keyboard.event.Event;
-import helium314.keyboard.event.HangulEventDecoder;
import helium314.keyboard.event.InputTransaction;
import helium314.keyboard.keyboard.Keyboard;
import helium314.keyboard.keyboard.KeyboardSwitcher;
@@ -44,11 +43,13 @@ import helium314.keyboard.latin.common.StringUtils;
import helium314.keyboard.latin.common.StringUtilsKt;
import helium314.keyboard.latin.common.SuggestionSpanUtilsKt;
import helium314.keyboard.latin.define.DebugFlags;
+import helium314.keyboard.latin.settings.Settings;
import helium314.keyboard.latin.settings.SettingsValues;
import helium314.keyboard.latin.settings.SpacingAndPunctuations;
import helium314.keyboard.latin.suggestions.SuggestionStripViewAccessor;
import helium314.keyboard.latin.utils.AsyncResultHolder;
import helium314.keyboard.latin.utils.InputTypeUtils;
+import helium314.keyboard.latin.utils.IntentUtils;
import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.utils.RecapitalizeStatus;
import helium314.keyboard.latin.utils.ScriptUtils;
@@ -71,7 +72,7 @@ public final class InputLogic {
final LatinIME mLatinIME;
private final SuggestionStripViewAccessor mSuggestionStripViewAccessor;
- @NonNull private InputLogicHandler mInputLogicHandler;
+ @NonNull private final InputLogicHandler mInputLogicHandler;
// TODO : make all these fields private as soon as possible.
// Current space state of the input method. This can be any of the above constants.
@@ -89,6 +90,7 @@ public final class InputLogic {
private int mDeleteCount;
private long mLastKeyTime;
+ // todo: this is not used, so either remove it or do something with it
public final TreeSet mCurrentlyPressedHardwareKeys = new TreeSet<>();
// Keeps track of most recently inserted text (multi-character key) for reverting
@@ -119,7 +121,7 @@ public final class InputLogic {
mSuggestionStripViewAccessor = suggestionStripViewAccessor;
mWordComposer = new WordComposer();
mConnection = new RichInputConnection(latinIME);
- mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
+ mInputLogicHandler = new InputLogicHandler(mLatinIME.mHandler, this);
mSuggest = new Suggest(dictionaryFacilitator);
mDictionaryFacilitator = dictionaryFacilitator;
}
@@ -129,7 +131,7 @@ public final class InputLogic {
*
* Call this when input starts or restarts in some editor (typically, in onStartInputView).
*
- * @param combiningSpec the combining spec string for this subtype
+ * @param combiningSpec the combining spec string for this subtype (from extra value)
* @param settingsValues the current settings values
*/
public void startInput(final String combiningSpec, final SettingsValues settingsValues) {
@@ -153,11 +155,7 @@ public final class InputLogic {
// editorInfo.initialSelStart is not the actual cursor position, so we try using some heuristics to find the correct position.
mConnection.tryFixIncorrectCursorPosition();
cancelDoubleSpacePeriodCountdown();
- if (InputLogicHandler.NULL_HANDLER == mInputLogicHandler) {
- mInputLogicHandler = new InputLogicHandler(mLatinIME, this);
- } else {
- mInputLogicHandler.reset();
- }
+ mInputLogicHandler.reset();
mConnection.requestCursorUpdates(true, true);
}
@@ -200,17 +198,6 @@ public final class InputLogic {
mInputLogicHandler.reset();
}
- // Normally this class just gets out of scope after the process ends, but in unit tests, we
- // create several instances of LatinIME in the same process, which results in several
- // instances of InputLogic. This cleans up the associated handler so that tests don't leak
- // handlers.
- public void recycle() {
- final InputLogicHandler inputLogicHandler = mInputLogicHandler;
- mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
- inputLogicHandler.destroy();
- mDictionaryFacilitator.closeDictionaries();
- }
-
/**
* React to a string input.
*
@@ -414,7 +401,11 @@ public final class InputLogic {
// Stop the last recapitalization, if started.
mRecapitalizeStatus.stop();
mWordBeingCorrectedByCursor = null;
- return true;
+
+ // we do not return true if
+ final boolean oneSidedSelectionMove = hasOrHadSelection
+ && ((oldSelEnd == newSelEnd && oldSelStart != newSelStart) || (oldSelEnd != newSelEnd && oldSelStart == newSelStart));
+ return !oneSidedSelectionMove;
}
public boolean moveCursorByAndReturnIfInsideComposingWord(int distance) {
@@ -439,24 +430,7 @@ public final class InputLogic {
final String currentKeyboardScript, final LatinIME.UIHandler handler) {
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);
- } else {
- mWordComposer.setHangul(false);
- processedEvent = mWordComposer.processEvent(event);
- }
+ final Event processedEvent = mWordComposer.processEvent(event);
final InputTransaction inputTransaction = new InputTransaction(settingsValues,
processedEvent, SystemClock.uptimeMillis(), mSpaceState,
getActualCapsMode(settingsValues, keyboardShiftMode));
@@ -675,7 +649,8 @@ public final class InputLogic {
*/
private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction,
final String currentKeyboardScript, final LatinIME.UIHandler handler) {
- switch (event.getMKeyCode()) {
+ final int keyCode = event.getMKeyCode();
+ switch (keyCode) {
case KeyCode.DELETE:
handleBackspaceEvent(event, inputTransaction, currentKeyboardScript);
// Backspace is a functional key, but it affects the contents of the editor.
@@ -718,7 +693,7 @@ public final class InputLogic {
case KeyCode.SHIFT_ENTER:
// todo: try using sendDownUpKeyEventWithMetaState() and remove the key code maybe
final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
- event.getMKeyCode(), 0, event.getMX(), event.getMY(), event.isKeyRepeat());
+ keyCode, 0, event.getMX(), event.getMY(), event.isKeyRepeat());
handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler);
// Shift + Enter is treated as a functional key but it results in adding a new
// line, so that does affect the contents of the editor.
@@ -749,23 +724,48 @@ public final class InputLogic {
if (mConnection.hasSelection()) {
mConnection.copyText(true);
// fake delete keypress to remove the text
- final Event backspaceEvent = LatinIME.createSoftwareKeypressEvent(KeyCode.DELETE, 0,
+ final Event backspaceEvent = Event.createSoftwareKeypressEvent(KeyCode.DELETE, 0,
event.getMX(), event.getMY(), event.isKeyRepeat());
handleBackspaceEvent(backspaceEvent, inputTransaction, currentKeyboardScript);
inputTransaction.setDidAffectContents();
}
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 | event.getMMetaState());
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 | event.getMMetaState());
break;
case KeyCode.MOVE_START_OF_PAGE:
- sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.META_CTRL_ON);
+ final int selectionEnd1 = mConnection.getExpectedSelectionEnd();
+ final int selectionStart1 = mConnection.getExpectedSelectionStart();
+ sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.META_CTRL_ON | event.getMMetaState());
+ if (mConnection.getExpectedSelectionStart() == selectionStart1 && mConnection.getExpectedSelectionEnd() == selectionEnd1) {
+ // unchanged -> try a different method (necessary for compose fields)
+ final int newEnd = (event.getMMetaState() & KeyEvent.META_SHIFT_MASK) != 0 ? selectionEnd1 : 0;
+ mConnection.setSelection(0, newEnd);
+ }
break;
case KeyCode.MOVE_END_OF_PAGE:
- sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_END, KeyEvent.META_CTRL_ON);
+ final int selectionStart2 = mConnection.getExpectedSelectionStart();
+ final int selectionEnd2 = mConnection.getExpectedSelectionEnd();
+ sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_MOVE_END, KeyEvent.META_CTRL_ON | event.getMMetaState());
+ if (mConnection.getExpectedSelectionStart() == selectionStart2 && mConnection.getExpectedSelectionEnd() == selectionEnd2) {
+ // unchanged, try fallback e.g. for compose fields that don't care about ctrl + end
+ // we just move to a very large index, and hope the field is prepared to deal with this
+ // getting the actual length of the text for setting the correct position can be tricky for some apps...
+ try {
+ final int newStart = (event.getMMetaState() & KeyEvent.META_SHIFT_MASK) != 0 ? selectionStart2 : Integer.MAX_VALUE;
+ mConnection.setSelection(newStart, Integer.MAX_VALUE);
+ } catch (Exception e) {
+ // better catch potential errors and just do nothing in this case
+ Log.i(TAG, "error when trying to move cursor to last position: " + e);
+ }
+ }
break;
case KeyCode.UNDO:
sendDownUpKeyEventWithMetaState(KeyEvent.KEYCODE_Z, KeyEvent.META_CTRL_ON);
@@ -779,31 +779,33 @@ public final class InputLogic {
case KeyCode.TIMESTAMP:
mLatinIME.onTextInput(TimestampKt.getTimestamp(mLatinIME));
break;
+ case KeyCode.SEND_INTENT_ONE, KeyCode.SEND_INTENT_TWO, KeyCode.SEND_INTENT_THREE:
+ IntentUtils.handleSendIntentKey(mLatinIME, event.getMKeyCode());
+ case KeyCode.IME_HIDE_UI:
+ mLatinIME.requestHideSelf(0);
+ break;
case KeyCode.VOICE_INPUT:
// switching to shortcut IME, shift state, keyboard,... is handled by LatinIME,
// {@link KeyboardSwitcher#onEvent(Event)}, or {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
// We need to switch to the shortcut IME. This is handled by LatinIME since the
// input logic has no business with IME switching.
- case KeyCode.CAPS_LOCK, KeyCode.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 (event.getMMetaState() != 0) {
- // need to convert codepoint to KeyEvent.KEYCODE_
- final int codeToConvert = event.getMKeyCode() < 0 ? event.getMKeyCode() : event.getMCodePoint();
- int keyEventCode = KeyCode.INSTANCE.toKeyEventCode(codeToConvert);
- if (keyEventCode != KeyEvent.KEYCODE_UNKNOWN)
- sendDownUpKeyEventWithMetaState(keyEventCode, event.getMMetaState());
- return; // never crash if user inputs sth we don't have a KeyEvent.KEYCODE for
- } else if (event.getMKeyCode() < 0) {
- int keyEventCode = KeyCode.INSTANCE.toKeyEventCode(event.getMKeyCode());
- if (keyEventCode != KeyEvent.KEYCODE_UNKNOWN) {
- sendDownUpKeyEvent(keyEventCode);
- return;
- }
+ if (KeyCode.INSTANCE.isModifier(keyCode))
+ return; // continuation of previous switch case above, but modifiers are held in a separate place
+ final int keyEventCode = keyCode > 0
+ ? keyCode
+ : event.getMCodePoint() >= 0 ? KeyCode.codePointToKeyEventCode(event.getMCodePoint())
+ : KeyCode.keyCodeToKeyEventCode(keyCode);
+ if (keyEventCode != KeyEvent.KEYCODE_UNKNOWN) {
+ sendDownUpKeyEventWithMetaState(keyEventCode, event.getMMetaState());
+ return;
}
- throw new RuntimeException("Unknown key code : " + event.getMKeyCode());
+ // unknown event
+ Log.e(TAG, "unknown event, key code: "+keyCode+", meta: "+event.getMMetaState());
+ if (DebugFlags.DEBUG_ENABLED)
+ throw new RuntimeException("Unknown event");
}
}
@@ -1650,26 +1652,24 @@ public final class InputLogic {
}
final AsyncResultHolder holder = new AsyncResultHolder<>("Suggest");
- mInputLogicHandler.getSuggestedWords(inputStyle, SuggestedWords.NOT_A_SEQUENCE_NUMBER,
- suggestedWords -> {
- final String typedWordString = mWordComposer.getTypedWord();
- final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo(
- typedWordString, "" /* prevWordsContext */,
- SuggestedWordInfo.MAX_SCORE,
- SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED,
- SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
- SuggestedWordInfo.NOT_A_CONFIDENCE);
- // Show new suggestions if we have at least one. Otherwise keep the old
- // suggestions with the new typed word. Exception: if the length of the
- // typed word is <= 1 (after a deletion typically) we clear old suggestions.
- if (suggestedWords.size() > 1 || typedWordString.length() <= 1) {
- holder.set(suggestedWords);
- } else {
- holder.set(retrieveOlderSuggestions(typedWordInfo, mSuggestedWords));
- }
+ mInputLogicHandler.getSuggestedWords(() -> getSuggestedWords(
+ inputStyle, SuggestedWords.NOT_A_SEQUENCE_NUMBER,
+ suggestedWords -> {
+ final String typedWordString = mWordComposer.getTypedWord();
+ final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo(
+ typedWordString, "", SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED,
+ Dictionary.DICTIONARY_USER_TYPED, SuggestedWordInfo.NOT_AN_INDEX, SuggestedWordInfo.NOT_A_CONFIDENCE
+ );
+ // Show new suggestions if we have at least one. Otherwise keep the old
+ // suggestions with the new typed word. Exception: if the length of the
+ // typed word is <= 1 (after a deletion typically) we clear old suggestions.
+ if (suggestedWords.size() > 1 || typedWordString.length() <= 1) {
+ holder.set(suggestedWords);
+ } else {
+ holder.set(retrieveOlderSuggestions(typedWordInfo, mSuggestedWords));
}
- );
-
+ }
+ ));
// This line may cause the current thread to wait.
final SuggestedWords suggestedWords = holder.get(null,
Constants.GET_SUGGESTED_WORDS_TIMEOUT);
@@ -1779,8 +1779,8 @@ public final class InputLogic {
// If there weren't any suggestion spans on this word, suggestions#size() will be 1
// if shouldIncludeResumedWordInSuggestions is true, 0 otherwise. In this case, we
// have no useful suggestions, so we will try to compute some for it instead.
- mInputLogicHandler.getSuggestedWords(Suggest.SESSION_ID_TYPING,
- SuggestedWords.NOT_A_SEQUENCE_NUMBER, this::doShowSuggestionsAndClearAutoCorrectionIndicator);
+ mInputLogicHandler.getSuggestedWords(() -> getSuggestedWords(Suggest.SESSION_ID_TYPING,
+ SuggestedWords.NOT_A_SEQUENCE_NUMBER, this::doShowSuggestionsAndClearAutoCorrectionIndicator));
} else {
// We found suggestion spans in the word. We'll create the SuggestedWords out of
// them, and make willAutoCorrect false. We make typedWordValid false, because the
@@ -2404,12 +2404,17 @@ public final class InputLogic {
return true;
}
- public void getSuggestedWords(final SettingsValues settingsValues,
- final Keyboard keyboard, final int keyboardShiftMode, final int inputStyle,
- final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
+ // we used to provide keyboard, settingsValues and keyboardShiftMode, but every time read it from current instance anyway
+ public void getSuggestedWords(final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
+ final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard();
+ if (keyboard == null) {
+ callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance());
+ return;
+ }
+ final SettingsValues settingsValues = Settings.getValues();
mWordComposer.adviseCapitalizedModeBeforeFetchingSuggestions(
- getActualCapsMode(settingsValues, keyboardShiftMode));
- mSuggest.getSuggestedWords(mWordComposer,
+ getActualCapsMode(settingsValues, KeyboardSwitcher.getInstance().getKeyboardShiftMode()));
+ final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
getNgramContextFromNthPreviousWordForSuggestion(
settingsValues.mSpacingAndPunctuations,
// Get the word on which we should search the bigrams. If we are composing
@@ -2419,7 +2424,8 @@ public final class InputLogic {
keyboard,
settingsValues.mSettingsValuesForSuggestion,
settingsValues.mAutoCorrectEnabled,
- inputStyle, sequenceNumber, callback);
+ inputStyle, sequenceNumber);
+ callback.onGetSuggestedWords(suggestedWords);
}
/**
diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.java
index 99b77fa3e..6e728a5a7 100644
--- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.java
+++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.java
@@ -11,7 +11,6 @@ import android.os.HandlerThread;
import android.os.Message;
import helium314.keyboard.latin.LatinIME;
-import helium314.keyboard.latin.Suggest.OnGetSuggestedWordsCallback;
import helium314.keyboard.latin.SuggestedWords;
import helium314.keyboard.latin.common.InputPointers;
@@ -20,48 +19,19 @@ import helium314.keyboard.latin.common.InputPointers;
*/
class InputLogicHandler implements Handler.Callback {
final Handler mNonUIThreadHandler;
- // TODO: remove this reference.
- final LatinIME mLatinIME;
+ final LatinIME.UIHandler mLatinIMEHandler;
final InputLogic mInputLogic;
private final Object mLock = new Object();
private boolean mInBatchInput; // synchronized using {@link #mLock}.
private static final int MSG_GET_SUGGESTED_WORDS = 1;
- // A handler that never does anything. This is used for cases where events come before anything
- // is initialized, though probably only the monkey can actually do this.
- public static final InputLogicHandler NULL_HANDLER = new InputLogicHandler() {
- @Override
- public void reset() {}
- @Override
- public boolean handleMessage(final Message msg) { return true; }
- @Override
- public void onStartBatchInput() {}
- @Override
- public void onUpdateBatchInput(final InputPointers batchPointers,
- final int sequenceNumber) {}
- @Override
- public void onCancelBatchInput() {}
- @Override
- public void updateTailBatchInput(final InputPointers batchPointers,
- final int sequenceNumber) {}
- @Override
- public void getSuggestedWords(final int sessionId, final int sequenceNumber,
- final OnGetSuggestedWordsCallback callback) {}
- };
-
- InputLogicHandler() {
- mNonUIThreadHandler = null;
- mLatinIME = null;
- mInputLogic = null;
- }
-
- public InputLogicHandler(final LatinIME latinIME, final InputLogic inputLogic) {
+ public InputLogicHandler(final LatinIME.UIHandler latinIMEHandler, final InputLogic inputLogic) {
final HandlerThread handlerThread = new HandlerThread(
InputLogicHandler.class.getSimpleName());
handlerThread.start();
mNonUIThreadHandler = new Handler(handlerThread.getLooper(), this);
- mLatinIME = latinIME;
+ mLatinIMEHandler = latinIMEHandler;
mInputLogic = inputLogic;
}
@@ -69,12 +39,6 @@ class InputLogicHandler implements Handler.Callback {
mNonUIThreadHandler.removeCallbacksAndMessages(null);
}
- // In unit tests, we create several instances of LatinIME, which results in several instances
- // of InputLogicHandler. To avoid these handlers lingering, we call this.
- public void destroy() {
- mNonUIThreadHandler.getLooper().quitSafely();
- }
-
/**
* Handle a message.
* @see android.os.Handler.Callback#handleMessage(android.os.Message)
@@ -83,7 +47,7 @@ class InputLogicHandler implements Handler.Callback {
@Override
public boolean handleMessage(final Message msg) {
if (msg.what == MSG_GET_SUGGESTED_WORDS)
- mLatinIME.getSuggestedWords(msg.arg1, msg.arg2, (OnGetSuggestedWordsCallback) msg.obj);
+ ((Runnable)msg.obj).run();
return true;
}
@@ -118,13 +82,14 @@ class InputLogicHandler implements Handler.Callback {
return;
}
mInputLogic.mWordComposer.setBatchInputPointers(batchPointers);
- final OnGetSuggestedWordsCallback callback = suggestedWords -> showGestureSuggestionsWithPreviewVisuals(suggestedWords, isTailBatchInput);
- getSuggestedWords(isTailBatchInput ? SuggestedWords.INPUT_STYLE_TAIL_BATCH
- : SuggestedWords.INPUT_STYLE_UPDATE_BATCH, sequenceNumber, callback);
+ getSuggestedWords(() -> mInputLogic.getSuggestedWords(
+ isTailBatchInput ? SuggestedWords.INPUT_STYLE_TAIL_BATCH : SuggestedWords.INPUT_STYLE_UPDATE_BATCH, sequenceNumber,
+ suggestedWords -> showGestureSuggestionsWithPreviewVisuals(suggestedWords, isTailBatchInput))
+ );
}
}
- void showGestureSuggestionsWithPreviewVisuals(final SuggestedWords suggestedWordsForBatchInput,
+ private void showGestureSuggestionsWithPreviewVisuals(final SuggestedWords suggestedWordsForBatchInput,
final boolean isTailBatchInput) {
final SuggestedWords suggestedWordsToShowSuggestions;
// We're now inside the callback. This always runs on the Non-UI thread,
@@ -138,13 +103,12 @@ class InputLogicHandler implements Handler.Callback {
} else {
suggestedWordsToShowSuggestions = suggestedWordsForBatchInput;
}
- mLatinIME.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWordsToShowSuggestions,
- isTailBatchInput /* dismissGestureFloatingPreviewText */);
+ mLatinIMEHandler.showGesturePreviewAndSuggestionStrip(suggestedWordsToShowSuggestions, isTailBatchInput);
if (isTailBatchInput) {
mInBatchInput = false;
// The following call schedules onEndBatchInputInternal
// to be called on the UI thread.
- mLatinIME.mHandler.showTailBatchInputResult(suggestedWordsToShowSuggestions);
+ mLatinIMEHandler.showTailBatchInputResult(suggestedWordsToShowSuggestions);
}
}
@@ -193,9 +157,7 @@ class InputLogicHandler implements Handler.Callback {
updateBatchInput(batchPointers, sequenceNumber, true);
}
- public void getSuggestedWords(final int inputStyle, final int sequenceNumber,
- final OnGetSuggestedWordsCallback callback) {
- mNonUIThreadHandler.obtainMessage(
- MSG_GET_SUGGESTED_WORDS, inputStyle, sequenceNumber, callback).sendToTarget();
+ public void getSuggestedWords(final Runnable callback) {
+ mNonUIThreadHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, callback).sendToTarget();
}
}
diff --git a/app/src/main/java/helium314/keyboard/latin/personalization/PersonalizationHelper.java b/app/src/main/java/helium314/keyboard/latin/personalization/PersonalizationHelper.java
index 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 377e19899..a73760205 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt
@@ -44,6 +44,7 @@ object Defaults {
LayoutType.CLIPBOARD_BOTTOM -> "clip_bottom_row"
}
+ private const val DEFAULT_SIZE_SCALE = 1.0f // 100%
const val PREF_THEME_STYLE = KeyboardTheme.STYLE_MATERIAL
const val PREF_ICON_STYLE = KeyboardTheme.STYLE_MATERIAL
const val PREF_THEME_COLORS = KeyboardTheme.THEME_LIGHT
@@ -57,6 +58,7 @@ object Defaults {
const val PREF_VIBRATE_ON = false
const val PREF_VIBRATE_IN_DND_MODE = false
const val PREF_SOUND_ON = false
+ const val PREF_SHOW_EMOJI_DESCRIPTIONS = true
@JvmField
var PREF_POPUP_ON = true
const val PREF_AUTO_CORRECTION = true
@@ -80,15 +82,18 @@ object Defaults {
"hu${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwerty"
const val PREF_ENABLE_SPLIT_KEYBOARD = false
const val PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE = false
- const val PREF_SPLIT_SPACER_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
- const val PREF_SPLIT_SPACER_SCALE_LANDSCAPE = SettingsValues.DEFAULT_SIZE_SCALE
- const val PREF_KEYBOARD_HEIGHT_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
- const val PREF_BOTTOM_PADDING_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
- const val PREF_BOTTOM_PADDING_SCALE_LANDSCAPE = 0f
- const val PREF_SIDE_PADDING_SCALE = 0f
- const val PREF_SIDE_PADDING_SCALE_LANDSCAPE = 0f
- const val PREF_FONT_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
- const val PREF_EMOJI_FONT_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
+ @JvmField
+ val PREF_SPLIT_SPACER_SCALE = Array(2) { DEFAULT_SIZE_SCALE }
+ @JvmField
+ val PREF_KEYBOARD_HEIGHT_SCALE = Array(2) { DEFAULT_SIZE_SCALE }
+ @JvmField
+ val PREF_BOTTOM_PADDING_SCALE = arrayOf(DEFAULT_SIZE_SCALE, 0f)
+ @JvmField
+ val PREF_SIDE_PADDING_SCALE = Array(4) { 0f }
+ const val PREF_FONT_SCALE = DEFAULT_SIZE_SCALE
+ const val PREF_EMOJI_FONT_SCALE = DEFAULT_SIZE_SCALE
+ const val PREF_EMOJI_KEY_FIT = true
+ const val PREF_EMOJI_SKIN_TONE = ""
const val PREF_SPACE_HORIZONTAL_SWIPE = "move_cursor"
const val PREF_SPACE_VERTICAL_SWIPE = "none"
const val PREF_DELETE_SWIPE = true
@@ -115,6 +120,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")
@@ -142,6 +148,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
@@ -155,7 +163,6 @@ object Defaults {
const val PREF_REMOVE_REDUNDANT_POPUPS = false
const val PREF_SPACE_BAR_TEXT = ""
const val PREF_TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss"
- @JvmField
val PREF_EMOJI_MAX_SDK = Build.VERSION.SDK_INT
const val PREF_EMOJI_RECENT_KEYS = ""
const val PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID = 0
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java
index 098b51b0d..cc72c893c 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;
@@ -66,6 +67,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_VIBRATE_ON = "vibrate_on";
public static final String PREF_VIBRATE_IN_DND_MODE = "vibrate_in_dnd_mode";
public static final String PREF_SOUND_ON = "sound_on";
+ public static final String PREF_SHOW_EMOJI_DESCRIPTIONS = "show_emoji_descriptions";
public static final String PREF_POPUP_ON = "popup_on";
public static final String PREF_AUTO_CORRECTION = "auto_correction";
public static final String PREF_MORE_AUTO_CORRECTION = "more_auto_correction";
@@ -86,22 +88,21 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_ADDITIONAL_SUBTYPES = "additional_subtypes";
public static final String PREF_ENABLE_SPLIT_KEYBOARD = "split_keyboard";
public static final String PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE = "split_keyboard_landscape";
- public static final String PREF_SPLIT_SPACER_SCALE = "split_spacer_scale";
- public static final String PREF_SPLIT_SPACER_SCALE_LANDSCAPE = "split_spacer_scale_landscape";
- public static final String PREF_KEYBOARD_HEIGHT_SCALE = "keyboard_height_scale";
- public static final String PREF_BOTTOM_PADDING_SCALE = "bottom_padding_scale";
- public static final String PREF_BOTTOM_PADDING_SCALE_LANDSCAPE = "bottom_padding_scale_landscape";
- public static final String PREF_SIDE_PADDING_SCALE = "side_padding_scale";
- public static final String PREF_SIDE_PADDING_SCALE_LANDSCAPE = "side_padding_scale_landscape";
+ public static final String PREF_SPLIT_SPACER_SCALE_PREFIX = "split_spacer_scale";
+ public static final String PREF_KEYBOARD_HEIGHT_SCALE_PREFIX = "keyboard_height_scale";
+ public static final String PREF_BOTTOM_PADDING_SCALE_PREFIX = "bottom_padding_scale";
+ public static final String PREF_SIDE_PADDING_SCALE_PREFIX = "side_padding_scale";
public static final String PREF_FONT_SCALE = "font_scale";
public static final String PREF_EMOJI_FONT_SCALE = "emoji_font_scale";
+ public static final String PREF_EMOJI_KEY_FIT = "emoji_key_fit";
+ public static final String PREF_EMOJI_SKIN_TONE = "emoji_skin_tone";
public static final String PREF_SPACE_HORIZONTAL_SWIPE = "horizontal_space_swipe";
public static final String PREF_SPACE_VERTICAL_SWIPE = "vertical_space_swipe";
public static final String PREF_DELETE_SWIPE = "delete_swipe";
public static final String PREF_AUTOSPACE_AFTER_PUNCTUATION = "autospace_after_punctuation";
public static final String PREF_AUTOSPACE_AFTER_SUGGESTION = "autospace_after_suggestion";
public static final String PREF_AUTOSPACE_AFTER_GESTURE_TYPING = "autospace_after_gesture_typing";
- public static final String PREF_AUTOSPACE_BEFORE_GESTURE_TYPING = "autospace_before_gesture_typing";
+ public static final String PREF_AUTOSPACE_BEFORE_GESTURE_TYPING = "autospace_before_gesture_typing";
public static final String PREF_SHIFT_REMOVES_AUTOSPACE = "shift_removes_autospace";
public static final String PREF_ALWAYS_INCOGNITO_MODE = "always_incognito_mode";
public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
@@ -120,12 +121,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_GESTURE_TRAIL_FADEOUT_DURATION = "gesture_trail_fadeout_duration";
public static final String PREF_SHOW_SETUP_WIZARD_ICON = "show_setup_wizard_icon";
public static final String PREF_USE_CONTACTS = "use_contacts";
+ public static final String PREF_USE_APPS = "use_apps";
public static final String PREFS_LONG_PRESS_SYMBOLS_FOR_NUMPAD = "long_press_symbols_for_numpad";
- // one-handed mode gravity, enablement and scale, stored separately per orientation
- public static final String PREF_ONE_HANDED_MODE_PREFIX = "one_handed_mode_enabled_p_";
- public static final String PREF_ONE_HANDED_GRAVITY_PREFIX = "one_handed_mode_gravity_p_";
- public static final String PREF_ONE_HANDED_SCALE_PREFIX = "one_handed_mode_scale_p_";
+ public static final String PREF_ONE_HANDED_MODE_PREFIX = "one_handed_mode_enabled";
+ public static final String PREF_ONE_HANDED_GRAVITY_PREFIX = "one_handed_mode_gravity";
+ public static final String PREF_ONE_HANDED_SCALE_PREFIX = "one_handed_mode_scale";
public static final String PREF_SHOW_NUMBER_ROW = "show_number_row";
public static final String PREF_LOCALIZED_NUMBER_ROW = "localized_number_row";
@@ -165,6 +166,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_REMOVE_REDUNDANT_POPUPS = "remove_redundant_popups";
public static final String PREF_SPACE_BAR_TEXT = "space_bar_text";
public static final String PREF_TIMESTAMP_FORMAT = "timestamp_format";
+ public static final String PREF_TOOLBAR_MODE = "toolbar_mode";
+ public static final String PREF_TOOLBAR_HIDING_GLOBAL = "toolbar_hiding_global";
// Emoji
public static final String PREF_EMOJI_MAX_SDK = "emoji_max_sdk";
@@ -321,6 +324,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
mPrefs.edit().putBoolean(Settings.PREF_ALWAYS_INCOGNITO_MODE, !oldValue).apply();
}
+ public static ToolbarMode readToolbarMode(final SharedPreferences prefs) {
+ return ToolbarMode.valueOf(prefs.getString(PREF_TOOLBAR_MODE, Defaults.PREF_TOOLBAR_MODE));
+ }
+
public static int readHorizontalSpaceSwipe(final SharedPreferences prefs) {
return switch (prefs.getString(PREF_SPACE_HORIZONTAL_SWIPE, Defaults.PREF_SPACE_HORIZONTAL_SWIPE)) {
case "move_cursor" -> KeyboardActionListener.SWIPE_MOVE_CURSOR;
@@ -355,31 +362,43 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return prefs.getBoolean(PREF_SHOW_SETUP_WIZARD_ICON, Defaults.PREF_SHOW_SETUP_WIZARD_ICON);
}
- public static boolean readOneHandedModeEnabled(final SharedPreferences prefs, final boolean isLandscape) {
- return prefs.getBoolean(PREF_ONE_HANDED_MODE_PREFIX + !isLandscape, Defaults.PREF_ONE_HANDED_MODE);
+ public static boolean readOneHandedModeEnabled(final SharedPreferences prefs, final boolean landscape, final boolean split) {
+ final int index = SettingsKt.findIndexOfDefaultSetting(landscape, split);
+ final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_MODE_PREFIX, index, 2);
+ return prefs.getBoolean(key, Defaults.PREF_ONE_HANDED_MODE);
}
public void writeOneHandedModeEnabled(final boolean enabled) {
- mPrefs.edit().putBoolean(PREF_ONE_HANDED_MODE_PREFIX +
- (mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), enabled).apply();
+ final boolean landscape = mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE;
+ final int index = SettingsKt.findIndexOfDefaultSetting(landscape, mSettingsValues.mIsSplitKeyboardEnabled);
+ final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_MODE_PREFIX, index, 2);
+ mPrefs.edit().putBoolean(key, enabled).apply();
}
- public static float readOneHandedModeScale(final SharedPreferences prefs, final boolean isLandscape) {
- return prefs.getFloat(PREF_ONE_HANDED_SCALE_PREFIX + !isLandscape, Defaults.PREF_ONE_HANDED_SCALE);
+ public static float readOneHandedModeScale(final SharedPreferences prefs, final boolean landscape, final boolean split) {
+ final int index = SettingsKt.findIndexOfDefaultSetting(landscape, split);
+ final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_SCALE_PREFIX, index, 2);
+ return prefs.getFloat(key, Defaults.PREF_ONE_HANDED_SCALE);
}
public void writeOneHandedModeScale(final Float scale) {
- mPrefs.edit().putFloat(PREF_ONE_HANDED_SCALE_PREFIX +
- (mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), scale).apply();
+ final boolean landscape = mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE;
+ final int index = SettingsKt.findIndexOfDefaultSetting(landscape, mSettingsValues.mIsSplitKeyboardEnabled);
+ final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_SCALE_PREFIX, index, 2);
+ mPrefs.edit().putFloat(key, scale).apply();
}
- public static int readOneHandedModeGravity(final SharedPreferences prefs, final boolean isLandscape) {
- return prefs.getInt(PREF_ONE_HANDED_GRAVITY_PREFIX + !isLandscape, Defaults.PREF_ONE_HANDED_GRAVITY);
+ public static int readOneHandedModeGravity(final SharedPreferences prefs, final boolean landscape, final boolean split) {
+ final int index = SettingsKt.findIndexOfDefaultSetting(landscape, split);
+ final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_GRAVITY_PREFIX, index, 2);
+ return prefs.getInt(key, Defaults.PREF_ONE_HANDED_GRAVITY);
}
public void writeOneHandedModeGravity(final int gravity) {
- mPrefs.edit().putInt(PREF_ONE_HANDED_GRAVITY_PREFIX +
- (mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT), gravity).apply();
+ final boolean landscape = mSettingsValues.mDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE;
+ final int index = SettingsKt.findIndexOfDefaultSetting(landscape, mSettingsValues.mIsSplitKeyboardEnabled);
+ final String key = SettingsKt.createPrefKeyForBooleanSettings(PREF_ONE_HANDED_GRAVITY_PREFIX, index, 2);
+ mPrefs.edit().putInt(key, gravity).apply();
}
public void writeSplitKeyboardEnabled(final boolean enabled, final boolean isLandscape) {
@@ -392,21 +411,32 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return prefs.getBoolean(pref, isLandscape ? Defaults.PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE : Defaults.PREF_ENABLE_SPLIT_KEYBOARD);
}
- public static float readSplitSpacerScale(final SharedPreferences prefs, final boolean isLandscape) {
- final String pref = isLandscape ? PREF_SPLIT_SPACER_SCALE_LANDSCAPE : PREF_SPLIT_SPACER_SCALE;
- return prefs.getFloat(pref, isLandscape ? Defaults.PREF_SPLIT_SPACER_SCALE_LANDSCAPE : Defaults.PREF_SPLIT_SPACER_SCALE);
+ public static float readSplitSpacerScale(final SharedPreferences prefs, final boolean landscape) {
+ final int index = SettingsKt.findIndexOfDefaultSetting(landscape);
+ final Float[] defaults = Defaults.PREF_SPLIT_SPACER_SCALE;
+ final float defaultValue = defaults[index];
+ return prefs.getFloat(SettingsKt.createPrefKeyForBooleanSettings(PREF_SPLIT_SPACER_SCALE_PREFIX, index, 1), defaultValue);
}
public static float readBottomPaddingScale(final SharedPreferences prefs, final boolean landscape) {
- if (landscape)
- return prefs.getFloat(PREF_BOTTOM_PADDING_SCALE_LANDSCAPE, Defaults.PREF_BOTTOM_PADDING_SCALE_LANDSCAPE);
- return prefs.getFloat(PREF_BOTTOM_PADDING_SCALE, Defaults.PREF_BOTTOM_PADDING_SCALE);
+ final int index = SettingsKt.findIndexOfDefaultSetting(landscape);
+ final Float[] defaults = Defaults.PREF_BOTTOM_PADDING_SCALE;
+ final float defaultValue = defaults[index];
+ return prefs.getFloat(SettingsKt.createPrefKeyForBooleanSettings(PREF_BOTTOM_PADDING_SCALE_PREFIX, index, 1), defaultValue);
}
- public static float readSidePaddingScale(final SharedPreferences prefs, final boolean landscape) {
- if (landscape)
- return prefs.getFloat(PREF_SIDE_PADDING_SCALE_LANDSCAPE, Defaults.PREF_SIDE_PADDING_SCALE_LANDSCAPE);
- return prefs.getFloat(PREF_SIDE_PADDING_SCALE, Defaults.PREF_SIDE_PADDING_SCALE);
+ public static float readSidePaddingScale(final SharedPreferences prefs, final boolean landscape, final boolean split) {
+ final int index = SettingsKt.findIndexOfDefaultSetting(landscape, split);
+ final Float[] defaults = Defaults.PREF_SIDE_PADDING_SCALE;
+ final float defaultValue = defaults[index];
+ return prefs.getFloat(SettingsKt.createPrefKeyForBooleanSettings(PREF_SIDE_PADDING_SCALE_PREFIX, index, 2), defaultValue);
+ }
+
+ public static float readHeightScale(final SharedPreferences prefs, final boolean landscape) {
+ final int index = SettingsKt.findIndexOfDefaultSetting(landscape);
+ final Float[] defaults = Defaults.PREF_KEYBOARD_HEIGHT_SCALE;
+ final float defaultValue = defaults[index];
+ return prefs.getFloat(SettingsKt.createPrefKeyForBooleanSettings(PREF_KEYBOARD_HEIGHT_SCALE_PREFIX, index, 1), defaultValue);
}
public static boolean readHasHardwareKeyboard(final Configuration conf) {
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt b/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt
index 5a472d058..d532a16a3 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt
@@ -14,3 +14,13 @@ fun customIconIds(context: Context, prefs: SharedPreferences) = customIconNames(
val id = runCatching { context.resources.getIdentifier(entry.value, "drawable", context.packageName) }.getOrNull()
id?.let { entry.key to it }
}
+
+/** Derive an index from a number of boolean [settingValues], used to access the matching default value in a defaults arraY */
+fun findIndexOfDefaultSetting(vararg settingValues: Boolean): Int {
+ var i = -1
+ return settingValues.sumOf { i++; if (it) 1.shl(i) else 0 }
+}
+
+/** Create pref key that is derived from a [number] of boolean conditions. The [index] is as created by [findIndexOfDefaultSetting]. */
+fun createPrefKeyForBooleanSettings(prefix: String, index: Int, number: Int): String =
+ "${prefix}_${Array(number) { index.shr(it) % 2 == 1 }.joinToString("_")}"
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
index 9fe0a3725..f750f1072 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
+++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.java
@@ -16,7 +16,6 @@ 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;
@@ -32,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;
@@ -42,8 +42,6 @@ import java.util.Locale;
*/
// Non-final for testing via mock library.
public class SettingsValues {
- public static final float DEFAULT_SIZE_SCALE = 1.0f; // 100%
-
// From resources:
public final SpacingAndPunctuations mSpacingAndPunctuations;
public final long mDoubleSpacePeriodTimeout;
@@ -56,6 +54,7 @@ public class SettingsValues {
public final boolean mVibrateOn;
public final boolean mVibrateInDndMode;
public final boolean mSoundOn;
+ public final boolean mShowEmojiDescriptions;
public final boolean mKeyPreviewPopupOn;
public final boolean mShowsVoiceInputKey;
public final boolean mLanguageSwitchKeyToOtherImes;
@@ -109,11 +108,14 @@ 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;
@@ -124,12 +126,15 @@ public class SettingsValues {
public final String mSpaceBarText;
public final float mFontSizeMultiplier;
public final float mFontSizeMultiplierEmoji;
+ public final boolean mEmojiKeyFit;
// From the input box
@NonNull
public final InputAttributes mInputAttributes;
// Deduced settings
+ public final boolean mSuggestionStripHiddenPerUserSettings;
+ public final boolean mSecondaryStripVisible;
public final int mKeypressVibrationDuration;
public final float mKeypressSoundVolume;
public final boolean mAutoCorrectionEnabledPerUserSettings;
@@ -144,14 +149,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) {
@@ -163,10 +164,13 @@ public class SettingsValues {
mInputAttributes = inputAttributes;
// Get the settings preferences
+ mToolbarMode = Settings.readToolbarMode(prefs);
+ mToolbarHidingGlobal = prefs.getBoolean(Settings.PREF_TOOLBAR_HIDING_GLOBAL, Defaults.PREF_TOOLBAR_HIDING_GLOBAL);
mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, Defaults.PREF_AUTO_CAP) && ScriptUtils.scriptSupportsUppercase(mLocale);
mVibrateOn = Settings.readVibrationEnabled(prefs);
mVibrateInDndMode = prefs.getBoolean(Settings.PREF_VIBRATE_IN_DND_MODE, Defaults.PREF_VIBRATE_IN_DND_MODE);
mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, Defaults.PREF_SOUND_ON);
+ mShowEmojiDescriptions = prefs.getBoolean(Settings.PREF_SHOW_EMOJI_DESCRIPTIONS, Defaults.PREF_SHOW_EMOJI_DESCRIPTIONS);
mKeyPreviewPopupOn = prefs.getBoolean(Settings.PREF_POPUP_ON, Defaults.PREF_POPUP_ON);
mSlidingKeyInputPreviewEnabled = prefs.getBoolean(
DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW, Defaults.PREF_SLIDING_KEY_INPUT_PREVIEW);
@@ -183,7 +187,7 @@ public class SettingsValues {
mShowTldPopupKeys = prefs.getBoolean(Settings.PREF_SHOW_TLD_POPUP_KEYS, Defaults.PREF_SHOW_TLD_POPUP_KEYS);
mSpaceForLangChange = prefs.getBoolean(Settings.PREF_SPACE_TO_CHANGE_LANG, Defaults.PREF_SPACE_TO_CHANGE_LANG);
mShowsEmojiKey = prefs.getBoolean(Settings.PREF_SHOW_EMOJI_KEY, Defaults.PREF_SHOW_EMOJI_KEY);
- mVarToolbarDirection = 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;
@@ -212,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
@@ -227,17 +231,18 @@ 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?
+ 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);
+ mKeyboardHeightScale = Settings.readHeightScale(prefs, isLandscape);
mSpaceSwipeHorizontal = Settings.readHorizontalSpaceSwipe(prefs);
mSpaceSwipeVertical = Settings.readVerticalSpaceSwipe(prefs);
mLanguageSwipeDistance = prefs.getInt(Settings.PREF_LANGUAGE_SWIPE_DISTANCE, Defaults.PREF_LANGUAGE_SWIPE_DISTANCE);
@@ -250,11 +255,11 @@ public class SettingsValues {
mClipboardHistoryEnabled = prefs.getBoolean(Settings.PREF_ENABLE_CLIPBOARD_HISTORY, Defaults.PREF_ENABLE_CLIPBOARD_HISTORY);
mClipboardHistoryRetentionTime = prefs.getInt(Settings.PREF_CLIPBOARD_HISTORY_RETENTION_TIME, Defaults.PREF_CLIPBOARD_HISTORY_RETENTION_TIME);
- mOneHandedModeEnabled = Settings.readOneHandedModeEnabled(prefs, isLandscape);
- mOneHandedModeGravity = Settings.readOneHandedModeGravity(prefs, isLandscape);
+ mOneHandedModeEnabled = Settings.readOneHandedModeEnabled(prefs, isLandscape, mIsSplitKeyboardEnabled);
+ mOneHandedModeGravity = Settings.readOneHandedModeGravity(prefs, isLandscape, mIsSplitKeyboardEnabled);
if (mOneHandedModeEnabled) {
final float baseScale = res.getFraction(R.fraction.config_one_handed_mode_width, 1, 1);
- final float extraScale = Settings.readOneHandedModeScale(prefs, isLandscape);
+ final float extraScale = Settings.readOneHandedModeScale(prefs, isLandscape, mIsSplitKeyboardEnabled);
mOneHandedModeScale = 1 - (1 - baseScale) * extraScale;
} else
mOneHandedModeScale = 1f;
@@ -268,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(
@@ -276,19 +282,19 @@ public class SettingsValues {
);
mSpacingAndPunctuations = new SpacingAndPunctuations(res, mUrlDetectionEnabled);
mBottomPaddingScale = Settings.readBottomPaddingScale(prefs, isLandscape);
- mSidePaddingScale = Settings.readSidePaddingScale(prefs, isLandscape);
+ mSidePaddingScale = Settings.readSidePaddingScale(prefs, isLandscape, mIsSplitKeyboardEnabled);
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() {
@@ -352,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/spellcheck/AndroidWordLevelSpellCheckerSession.java b/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index 94cd176d8..9f5ab79ec 100644
--- a/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -335,6 +335,8 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
}
final WordComposer composer = new WordComposer();
+ if (mLocale.getLanguage().equals("ko"))
+ composer.restartCombining("hangul");
final int[] codePoints = StringUtils.toCodePointArray(text);
final int[] coordinates;
coordinates = keyboard.getCoordinates(codePoints);
diff --git a/app/src/main/java/helium314/keyboard/latin/suggestions/MoreSuggestions.java b/app/src/main/java/helium314/keyboard/latin/suggestions/MoreSuggestions.java
index 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..ae55648c8
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/suggestions/SuggestionStripView.kt
@@ -0,0 +1,532 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+package helium314.keyboard.latin.suggestions
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.GestureDetector
+import android.view.GestureDetector.SimpleOnGestureListener
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnLongClickListener
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
+import android.widget.ImageButton
+import android.widget.LinearLayout
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
+import helium314.keyboard.compat.isDeviceLocked
+import helium314.keyboard.keyboard.KeyboardSwitcher
+import helium314.keyboard.keyboard.internal.KeyboardIconsSet
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
+import helium314.keyboard.latin.AudioAndHapticFeedbackManager
+import helium314.keyboard.latin.Dictionary
+import helium314.keyboard.latin.R
+import helium314.keyboard.latin.SuggestedWords
+import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
+import helium314.keyboard.latin.common.ColorType
+import helium314.keyboard.latin.common.Colors
+import helium314.keyboard.latin.common.Constants
+import helium314.keyboard.latin.define.DebugFlags
+import helium314.keyboard.latin.settings.DebugSettings
+import helium314.keyboard.latin.settings.Defaults
+import helium314.keyboard.latin.settings.Settings
+import helium314.keyboard.latin.utils.Log
+import helium314.keyboard.latin.utils.ToolbarKey
+import helium314.keyboard.latin.utils.ToolbarMode
+import helium314.keyboard.latin.utils.addPinnedKey
+import helium314.keyboard.latin.utils.createToolbarKey
+import helium314.keyboard.latin.utils.getCodeForToolbarKey
+import helium314.keyboard.latin.utils.getCodeForToolbarKeyLongClick
+import helium314.keyboard.latin.utils.getEnabledToolbarKeys
+import helium314.keyboard.latin.utils.getPinnedToolbarKeys
+import helium314.keyboard.latin.utils.prefs
+import helium314.keyboard.latin.utils.removeFirst
+import helium314.keyboard.latin.utils.removePinnedKey
+import helium314.keyboard.latin.utils.setToolbarButtonsActivatedStateOnPrefChange
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.math.min
+
+@SuppressLint("InflateParams")
+class SuggestionStripView(context: Context, attrs: AttributeSet?, defStyle: Int) :
+ RelativeLayout(context, attrs, defStyle), View.OnClickListener, OnLongClickListener, OnSharedPreferenceChangeListener {
+
+ /** Construct a [SuggestionStripView] for showing suggestions to be picked by the user. */
+ constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.attr.suggestionStripViewStyle)
+
+ interface Listener {
+ fun pickSuggestionManually(word: SuggestedWordInfo?)
+ fun onCodeInput(primaryCode: Int, x: Int, y: Int, isKeyRepeat: Boolean)
+ fun removeSuggestion(word: String?)
+ }
+
+ private val moreSuggestionsContainer: View
+ private val wordViews = ArrayList()
+ private val debugInfoViews = ArrayList()
+ private val dividerViews = ArrayList()
+
+ init {
+ val inflater = LayoutInflater.from(context)
+ inflater.inflate(R.layout.suggestions_strip, this)
+ moreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null)
+
+ val colors = Settings.getValues().mColors
+ colors.setBackground(this, ColorType.STRIP_BACKGROUND)
+ val customTypeface = Settings.getInstance().customTypeface
+ for (pos in 0..(R.id.suggestions_strip_toolbar_key)
+ private val incognitoIcon = KeyboardIconsSet.instance.getNewDrawable(ToolbarKey.INCOGNITO.name, context)
+ private val toolbarArrowIcon = KeyboardIconsSet.instance.getNewDrawable(KeyboardIconsSet.NAME_TOOLBAR_KEY, context)
+ private val defaultToolbarBackground: Drawable = toolbarExpandKey.background
+ private val enabledToolKeyBackground = GradientDrawable()
+ private var direction = 1 // 1 if LTR, -1 if RTL
+ init {
+ val colors = Settings.getValues().mColors
+
+ // expand key
+ // weird way of setting size (default is config_suggestions_strip_edge_key_width)
+ // but better not change it or people will complain
+ val toolbarHeight = min(toolbarExpandKey.layoutParams.height, resources.getDimension(R.dimen.config_suggestions_strip_height).toInt())
+ toolbarExpandKey.layoutParams.height = toolbarHeight
+ toolbarExpandKey.layoutParams.width = toolbarHeight // we want it square
+ colors.setBackground(toolbarExpandKey, ColorType.STRIP_BACKGROUND) // necessary because background is re-used for defaultToolbarBackground
+ colors.setColor(toolbarExpandKey, ColorType.TOOL_BAR_EXPAND_KEY)
+ colors.setColor(toolbarExpandKey.background, ColorType.TOOL_BAR_EXPAND_KEY_BACKGROUND)
+
+ // background indicator for pinned keys
+ val color = colors.get(ColorType.TOOL_BAR_KEY_ENABLED_BACKGROUND) or -0x1000000 // ignore alpha (in Java this is more readable 0xFF000000)
+ enabledToolKeyBackground.colors = intArrayOf(color, Color.TRANSPARENT)
+ enabledToolKeyBackground.gradientType = GradientDrawable.RADIAL_GRADIENT
+ enabledToolKeyBackground.gradientRadius = resources.getDimensionPixelSize(R.dimen.config_suggestions_strip_height) / 2.1f
+
+ val mToolbarMode = Settings.getValues().mToolbarMode
+ if (mToolbarMode == ToolbarMode.TOOLBAR_KEYS) {
+ setToolbarVisibility(true)
+ }
+
+ // toolbar keys setup
+ val toolbarKeyLayoutParams = LinearLayout.LayoutParams(
+ resources.getDimensionPixelSize(R.dimen.config_suggestions_strip_edge_key_width),
+ LinearLayout.LayoutParams.MATCH_PARENT
+ )
+ if (mToolbarMode == ToolbarMode.TOOLBAR_KEYS || mToolbarMode == ToolbarMode.EXPANDABLE) {
+ for (key in getEnabledToolbarKeys(context.prefs())) {
+ val button = createToolbarKey(context, key)
+ button.layoutParams = toolbarKeyLayoutParams
+ setupKey(button, colors)
+ toolbar.addView(button)
+ }
+ }
+ if (!Settings.getValues().mSuggestionStripHiddenPerUserSettings) {
+ for (pinnedKey in getPinnedToolbarKeys(context.prefs())) {
+ val button = createToolbarKey(context, pinnedKey)
+ button.layoutParams = toolbarKeyLayoutParams
+ setupKey(button, colors)
+ pinnedKeys.addView(button)
+ val pinnedKeyInToolbar = toolbar.findViewWithTag(pinnedKey)
+ if (pinnedKeyInToolbar != null && Settings.getValues().mQuickPinToolbarKeys)
+ pinnedKeyInToolbar.background = enabledToolKeyBackground
+ }
+ }
+
+ updateKeys()
+ }
+
+ private lateinit var listener: Listener
+ private var suggestedWords = SuggestedWords.getEmptyInstance()
+ private var startIndexOfMoreSuggestions = 0
+ private var isExternalSuggestionVisible = false // Required to disable the more suggestions if other suggestions are visible
+ private val layoutHelper = SuggestionStripLayoutHelper(context, attrs, defStyle, wordViews, dividerViews, debugInfoViews)
+ private val moreSuggestionsView = moreSuggestionsContainer.findViewById(R.id.more_suggestions_view).apply {
+ val slidingListener = object : SimpleOnGestureListener() {
+ override fun onScroll(down: MotionEvent?, me: MotionEvent, deltaX: Float, deltaY: Float): Boolean {
+ if (down == null) return false
+ val dy = me.y - down.y
+ return if (toolbarContainer.visibility != VISIBLE && deltaY > 0 && dy < 0) showMoreSuggestions()
+ else false
+ }
+ }
+ gestureDetector = GestureDetector(context, slidingListener)
+ }
+
+ // public stuff
+
+ val isShowingMoreSuggestionPanel get() = moreSuggestionsView.isShowingInParent
+
+ /** A connection back to the input method. */
+ fun setListener(newListener: Listener, inputView: View) {
+ listener = newListener
+ moreSuggestionsView.listener = newListener
+ moreSuggestionsView.mainKeyboardView = inputView.findViewById(R.id.keyboard_view)
+ }
+
+ fun setRtl(isRtlLanguage: Boolean) {
+ val newLayoutDirection: Int
+ if (!Settings.getValues().mVarToolbarDirection)
+ newLayoutDirection = LAYOUT_DIRECTION_LOCALE
+ else {
+ newLayoutDirection = if (isRtlLanguage) LAYOUT_DIRECTION_RTL else LAYOUT_DIRECTION_LTR
+ direction = if (isRtlLanguage) -1 else 1
+ toolbarExpandKey.scaleX = (if (toolbarContainer.visibility != VISIBLE) 1f else -1f) * direction
+ }
+ layoutDirection = newLayoutDirection
+ suggestionsStrip.layoutDirection = newLayoutDirection
+ }
+
+ fun setToolbarVisibility(toolbarVisible: Boolean) {
+ // avoid showing toolbar keys when locked
+ val locked = isDeviceLocked(context)
+ pinnedKeys.isVisible = !locked && !toolbarVisible
+ suggestionsStrip.isVisible = locked || !toolbarVisible
+ toolbarContainer.isVisible = !locked && toolbarVisible
+
+ if (DEBUG_SUGGESTIONS) {
+ for (view in debugInfoViews) {
+ view.visibility = suggestionsStrip.visibility
+ }
+ }
+
+ toolbarExpandKey.scaleX = (if (toolbarVisible && !locked) -1f else 1f) * direction
+ }
+
+ fun setSuggestions(suggestions: SuggestedWords, isRtlLanguage: Boolean) {
+ clear()
+ setRtl(isRtlLanguage)
+ suggestedWords = suggestions
+ startIndexOfMoreSuggestions = layoutHelper.layoutAndReturnStartIndexOfMoreSuggestions(
+ context, suggestedWords, suggestionsStrip, this
+ )
+ isExternalSuggestionVisible = false
+ updateKeys();
+ }
+
+ fun setExternalSuggestionView(view: View?) {
+ clear()
+ isExternalSuggestionVisible = true
+ suggestionsStrip.addView(view)
+ if (Settings.getValues().mAutoHideToolbar) setToolbarVisibility(false)
+ }
+
+ fun setMoreSuggestionsHeight(remainingHeight: Int) {
+ layoutHelper.setMoreSuggestionsHeight(remainingHeight)
+ }
+
+ fun dismissMoreSuggestionsPanel() {
+ moreSuggestionsView.dismissPopupKeysPanel()
+ }
+
+ // overrides: necessarily public, but not used from outside
+
+ override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String?) {
+ setToolbarButtonsActivatedStateOnPrefChange(pinnedKeys, key)
+ setToolbarButtonsActivatedStateOnPrefChange(toolbar, key)
+ }
+
+ override fun onVisibilityChanged(view: View, visibility: Int) {
+ super.onVisibilityChanged(view, visibility)
+ // workaround for a bug with inline suggestions views that just keep showing up otherwise, https://github.com/Helium314/HeliBoard/pull/386
+ if (view === this)
+ suggestionsStrip.visibility = visibility
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ dismissMoreSuggestionsPanel()
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ // Called by the framework when the size is known. Show the important notice if applicable.
+ // This may be overridden by showing suggestions later, if applicable.
+ }
+
+ override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean {
+ // Don't populate accessibility event with suggested words and voice key.
+ return true
+ }
+
+ override fun onInterceptTouchEvent(motionEvent: MotionEvent): Boolean {
+ // Disable More Suggestions if external suggestions are visible
+ if (isExternalSuggestionVisible) {
+ return false
+ }
+ // Detecting sliding up finger to show MoreSuggestionsView.
+ return moreSuggestionsView.shouldInterceptTouchEvent(motionEvent)
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
+ moreSuggestionsView.touchEvent(motionEvent)
+ return true
+ }
+
+ override fun onClick(view: View) {
+ AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(KeyCode.NOT_SPECIFIED, this)
+ val tag = view.tag
+ if (tag is ToolbarKey) {
+ val code = getCodeForToolbarKey(tag)
+ if (code != KeyCode.UNSPECIFIED) {
+ Log.d(TAG, "click toolbar key $tag")
+ listener.onCodeInput(code, Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, false)
+ if (tag === ToolbarKey.INCOGNITO) updateKeys() // update expand key icon
+ return
+ }
+ }
+ if (view === toolbarExpandKey) {
+ setToolbarVisibility(toolbarContainer.visibility != VISIBLE)
+ }
+
+ // tag for word views is set in SuggestionStripLayoutHelper (setupWordViewsTextAndColor, layoutPunctuationSuggestions)
+ if (tag is Int) {
+ if (tag >= suggestedWords.size()) {
+ return
+ }
+ val wordInfo = suggestedWords.getInfo(tag)
+ listener.pickSuggestionManually(wordInfo)
+ }
+ }
+
+ override fun onLongClick(view: View): Boolean {
+ AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(Constants.NOT_A_CODE, this)
+ if (view.tag is ToolbarKey) {
+ onLongClickToolbarKey(view)
+ return true
+ }
+ return if (view is TextView && wordViews.contains(view)) {
+ onLongClickSuggestion(view)
+ } else {
+ showMoreSuggestions()
+ }
+ }
+
+ // actually private stuff
+
+ private fun onLongClickToolbarKey(view: View) {
+ val tag = view.tag as? ToolbarKey ?: return
+ if (!Settings.getValues().mQuickPinToolbarKeys || view.parent === pinnedKeys) {
+ val longClickCode = getCodeForToolbarKeyLongClick(tag)
+ if (longClickCode != KeyCode.UNSPECIFIED) {
+ listener.onCodeInput(longClickCode, Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, false)
+ }
+ } else if (view.parent === toolbar) {
+ val pinnedKeyView = pinnedKeys.findViewWithTag(tag)
+ if (pinnedKeyView == null) {
+ addKeyToPinnedKeys(tag)
+ toolbar.findViewWithTag(tag).background = enabledToolKeyBackground
+ addPinnedKey(context.prefs(), tag)
+ } else {
+ removePinnedKey(context.prefs(), tag)
+ toolbar.findViewWithTag(tag).background = defaultToolbarBackground.constantState?.newDrawable(resources)
+ pinnedKeys.removeView(pinnedKeyView)
+ }
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility") // no need for View#performClick, we only return false mostly anyway
+ private fun onLongClickSuggestion(wordView: TextView): Boolean {
+ var showIcon = true
+ if (wordView.tag is Int) {
+ val index = wordView.tag as Int
+ if (index < suggestedWords.size() && suggestedWords.getInfo(index).mSourceDict == Dictionary.DICTIONARY_USER_TYPED)
+ showIcon = false
+ }
+ if (showIcon) {
+ val icon = KeyboardIconsSet.instance.getNewDrawable(KeyboardIconsSet.NAME_BIN, context)!!
+ Settings.getValues().mColors.setColor(icon, ColorType.REMOVE_SUGGESTION_ICON)
+ val w = icon.intrinsicWidth
+ val h = icon.intrinsicHeight
+ wordView.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
+ wordView.ellipsize = TextUtils.TruncateAt.END
+ val downOk = AtomicBoolean(false)
+ wordView.setOnTouchListener { _, motionEvent ->
+ if (motionEvent.action == MotionEvent.ACTION_UP && downOk.get()) {
+ val x = motionEvent.x
+ val y = motionEvent.y
+ if (0 < x && x < w && 0 < y && y < h) {
+ removeSuggestion(wordView)
+ wordView.cancelLongPress()
+ wordView.isPressed = false
+ return@setOnTouchListener true
+ }
+ } else if (motionEvent.action == MotionEvent.ACTION_DOWN) {
+ val x = motionEvent.x
+ val y = motionEvent.y
+ if (0 < x && x < w && 0 < y && y < h) {
+ downOk.set(true)
+ }
+ }
+ false
+ }
+ }
+ if (DebugFlags.DEBUG_ENABLED && (isShowingMoreSuggestionPanel || !showMoreSuggestions())) {
+ showSourceDict(wordView)
+ return true
+ }
+ return showMoreSuggestions()
+ }
+
+ private fun showMoreSuggestions(): Boolean {
+ if (suggestedWords.size() <= startIndexOfMoreSuggestions) {
+ return false
+ }
+ if (!moreSuggestionsView.show(
+ suggestedWords, startIndexOfMoreSuggestions, moreSuggestionsContainer, layoutHelper, this
+ ))
+ return false
+ for (i in 0..= suggestedWords.size()) return
+ val info = suggestedWords.getInfo(index)
+ if (info.word != word) return
+
+ val text = info.mSourceDict.mDictType + ":" + info.mSourceDict.mLocale
+ if (isShowingMoreSuggestionPanel) {
+ moreSuggestionsView.dismissPopupKeysPanel()
+ }
+ KeyboardSwitcher.getInstance().showToast(text, true)
+ }
+
+ private fun removeSuggestion(wordView: TextView) {
+ val word = wordView.text.toString()
+ listener.removeSuggestion(word)
+ moreSuggestionsView.dismissPopupKeysPanel()
+ // show suggestions, but without the removed word
+ val suggestedWordInfos = ArrayList()
+ for (i in 0..(ToolbarKey.VOICE)?.isVisible = show
+ pinnedKeys.findViewWithTag(ToolbarKey.VOICE)?.isVisible = show
+ }
+
+ private fun updateKeys() {
+ updateVoiceKey()
+ val settingsValues = Settings.getValues()
+
+ val toolbarIsExpandable = settingsValues.mToolbarMode == ToolbarMode.EXPANDABLE
+ if (settingsValues.mIncognitoModeEnabled) {
+ toolbarExpandKey.setImageDrawable(incognitoIcon)
+ toolbarExpandKey.isVisible = true
+ } else {
+ toolbarExpandKey.setImageDrawable(toolbarArrowIcon)
+ toolbarExpandKey.isVisible = toolbarIsExpandable
+ }
+
+ // hide pinned keys if device is locked, and avoid expanding toolbar
+ val hideToolbarKeys = isDeviceLocked(context)
+ toolbarExpandKey.setOnClickListener(if (hideToolbarKeys || !toolbarIsExpandable) null else this)
+ pinnedKeys.visibility = if (hideToolbarKeys) GONE else suggestionsStrip.visibility
+ isExternalSuggestionVisible = false
+ }
+
+ private fun addKeyToPinnedKeys(pinnedKey: ToolbarKey) {
+ val original = toolbar.findViewWithTag(pinnedKey) ?: return
+ // copy the original key to a new ImageButton
+ val copy = ImageButton(context, null, R.attr.suggestionWordStyle)
+ copy.tag = pinnedKey
+ copy.scaleType = original.scaleType
+ copy.scaleX = original.scaleX
+ copy.scaleY = original.scaleY
+ copy.contentDescription = original.contentDescription
+ copy.setImageDrawable(original.drawable)
+ copy.layoutParams = original.layoutParams
+ copy.isActivated = original.isActivated
+ setupKey(copy, Settings.getValues().mColors)
+ pinnedKeys.addView(copy)
+ }
+
+ private fun setupKey(view: ImageButton, colors: Colors) {
+ view.setOnClickListener(this)
+ view.setOnLongClickListener(this)
+ colors.setColor(view, ColorType.TOOL_BAR_KEY)
+ colors.setBackground(view, ColorType.STRIP_BACKGROUND)
+ }
+
+ companion object {
+ @JvmField
+ var DEBUG_SUGGESTIONS = false
+ private const val DEBUG_INFO_TEXT_SIZE_IN_DIP = 6.5f
+ private val TAG = SuggestionStripView::class.java.simpleName
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.java
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..05417fdb3
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryInfoUtils.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ * modified
+ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
+ */
+package helium314.keyboard.latin.utils
+
+import android.content.Context
+import android.text.TextUtils
+import com.android.inputmethod.latin.utils.BinaryDictionaryUtils
+import helium314.keyboard.latin.common.FileUtils
+import helium314.keyboard.latin.common.LocaleUtils.constructLocale
+import helium314.keyboard.latin.common.loopOverCodePoints
+import helium314.keyboard.latin.define.DecoderSpecificConstants
+import helium314.keyboard.latin.makedict.DictionaryHeader
+import helium314.keyboard.latin.makedict.UnsupportedFormatException
+import helium314.keyboard.latin.settings.SpacingAndPunctuations
+import java.io.File
+import java.io.IOException
+import java.util.Locale
+
+/** encapsulates the logic for the Latin-IME side of dictionary information management */
+object DictionaryInfoUtils {
+ private val TAG = DictionaryInfoUtils::class.java.simpleName
+ const val DEFAULT_MAIN_DICT = "main"
+ const val USER_DICTIONARY_SUFFIX = "user.dict"
+ const val MAIN_DICT_PREFIX = DEFAULT_MAIN_DICT + "_"
+ const val ASSETS_DICTIONARY_FOLDER = "dicts"
+ const val MAIN_DICT_FILE_NAME = DEFAULT_MAIN_DICT + ".dict"
+ private const val MAX_HEX_DIGITS_FOR_CODEPOINT = 6 // unicode is limited to 21 bits
+
+ /**
+ * Returns whether we may want to use this character as part of a file name.
+ * This basically only accepts ascii letters and numbers, and rejects everything else.
+ */
+ private fun isFileNameCharacter(codePoint: Int): Boolean {
+ if (codePoint in 0x30..0x39) return true // Digit
+ if (codePoint in 0x41..0x5A) return true // Uppercase
+ if (codePoint in 0x61..0x7A) return true // Lowercase
+ return codePoint == '_'.code || codePoint == '-'.code
+ }
+
+ /**
+ * Escapes a string for any characters that may be suspicious for a file or directory name.
+ *
+ * Concretely this does a sort of URL-encoding except it will encode everything that's not
+ * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
+ * we cannot allow here)
+ */
+ private fun replaceFileNameDangerousCharacters(name: String): String {
+ // This assumes '%' is fully available as a non-separator, normal
+ // character in a file name. This is probably true for all file systems.
+ val sb = StringBuilder()
+ loopOverCodePoints(name) { codePoint, _ ->
+ if (isFileNameCharacter(codePoint)) {
+ sb.appendCodePoint(codePoint)
+ } else {
+ sb.append(String.format(Locale.US, "%%%1$0" + MAX_HEX_DIGITS_FOR_CODEPOINT + "x", codePoint))
+ }
+ false
+ }
+ return sb.toString()
+ }
+
+ // we cache the extracted dictionaries in filesDir, because actual cache might be cleared at
+ // any time, and we can't permanently check whether the dictionary still exists
+ fun getWordListCacheDirectory(context: Context): String = context.filesDir.toString() + File.separator + "dicts"
+
+ /** Reverse escaping done by replaceFileNameDangerousCharacters. */
+ fun getWordListIdFromFileName(fname: String): String {
+ val sb = StringBuilder()
+ val fnameLength = fname.length
+ var i = 0
+ while (i < fnameLength) {
+ val codePoint = fname.codePointAt(i)
+ if ('%'.code != codePoint) {
+ sb.appendCodePoint(codePoint)
+ } else {
+ // + 1 to pass the % sign
+ val encodedCodePoint = fname.substring(i + 1, i + 1 + MAX_HEX_DIGITS_FOR_CODEPOINT).toInt(16)
+ i += MAX_HEX_DIGITS_FOR_CODEPOINT
+ sb.appendCodePoint(encodedCodePoint)
+ }
+ i = fname.offsetByCodePoints(i, 1)
+ }
+ return sb.toString()
+ }
+
+ /** Helper method to the list of cache directories, one for each distinct locale. */
+ fun getCacheDirectories(context: Context) = File(getWordListCacheDirectory(context)).listFiles().orEmpty()
+
+ /** Find out the cache directory associated with a specific locale. */
+ fun getCacheDirectoryForLocale(locale: Locale, context: Context): String? {
+ val relativeDirectoryName = replaceFileNameDangerousCharacters(locale.toLanguageTag())
+ val absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator + relativeDirectoryName
+ val directory = File(absoluteDirectoryName)
+ if (!directory.exists() && !directory.mkdirs()) {
+ Log.e(TAG, "Could not create the directory for locale $locale")
+ return null
+ }
+ return absoluteDirectoryName
+ }
+
+ @JvmStatic
+ fun getCachedDictForLocaleAndType(locale: Locale, type: String, context: Context): File? =
+ getCachedDictsForLocale(locale, context).firstOrNull { it.name.substringBefore("_") == type }
+
+ fun getCachedDictsForLocale(locale: Locale, context: Context) =
+ getCacheDirectoryForLocale(locale, context)?.let { File(it).listFiles() }.orEmpty()
+
+ fun getDictionaryFileHeaderOrNull(file: File, offset: Long, length: Long): DictionaryHeader? {
+ return try {
+ BinaryDictionaryUtils.getHeaderWithOffsetAndLength(file, offset, length)
+ } catch (e: UnsupportedFormatException) {
+ null
+ } catch (e: IOException) {
+ null
+ }
+ }
+
+ fun getDictionaryFileHeaderOrNull(file: File): DictionaryHeader? {
+ return try {
+ BinaryDictionaryUtils.getHeader(file)
+ } catch (e: UnsupportedFormatException) {
+ null
+ } catch (e: IOException) {
+ null
+ }
+ }
+
+ /**
+ * Returns the locale for a dictionary file name stored in assets.
+ *
+ * Assumes file name main_[locale].dict
+ * Returns the locale, or null if file name does not match the pattern
+ */
+ fun extractLocaleFromAssetsDictionaryFile(dictionaryFileName: String): Locale {
+ if (dictionaryFileName.contains('_') && !dictionaryFileName.contains('.'))
+ throw IllegalStateException("invalid asset dictionary name $dictionaryFileName")
+ return dictionaryFileName.substringAfter("_").substringBefore(".").constructLocale()
+ }
+
+ // actually we could extract assets dicts to unprotected storage
+ fun extractAssetsDictionary(dictionaryFileName: String, locale: Locale, context: Context): File? {
+ val cacheDir = getCacheDirectoryForLocale(locale, context) ?: return null
+ val targetFile = File(cacheDir, "${dictionaryFileName.substringBefore("_")}.dict")
+ try {
+ FileUtils.copyStreamToNewFile(
+ context.assets.open(ASSETS_DICTIONARY_FOLDER + File.separator + dictionaryFileName),
+ targetFile
+ )
+ } catch (e: IOException) {
+ Log.e(TAG, "Could not extract assets dictionary $dictionaryFileName")
+ return null
+ }
+ return targetFile
+ }
+
+ fun getAssetsDictionaryList(context: Context): Array? = try {
+ context.assets.list(ASSETS_DICTIONARY_FOLDER)
+ } catch (e: IOException) {
+ null
+ }
+
+ @JvmStatic
+ fun looksValidForDictionaryInsertion(text: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
+ if (TextUtils.isEmpty(text)) {
+ return false
+ }
+ if (text.length > DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH) {
+ return false
+ }
+ var digitCount = 0
+ loopOverCodePoints(text) { codePoint, charCount ->
+ if (Character.isDigit(codePoint)) {
+ // Count digits: see below
+ digitCount += charCount
+ return@loopOverCodePoints false
+ }
+ if (!spacingAndPunctuations.isWordCodePoint(codePoint)) {
+ return false
+ }
+ false
+ }
+ // We reject strings entirely comprised of digits to avoid using PIN codes or credit
+ // card numbers. It would come in handy for word prediction though; a good example is
+ // when writing one's address where the street number is usually quite discriminative,
+ // as well as the postal code.
+ return digitCount < text.length
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/DictionaryUtils.kt
index 3bbb310c4..aa794f27c 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
@@ -87,7 +86,6 @@ fun MissingDictionaryDialog(onDismissRequest: () -> Unit, locale: Locale) {
fun createDictionaryTextAnnotated(locale: Locale): AnnotatedString {
val knownDicts = mutableListOf>()
val builder = AnnotatedString.Builder()
- builder.appendLine(stringResource(R.string.dictionary_available))
val context = LocalContext.current
context.assets.open("dictionaries_in_dict_repo.csv").reader().forEachLine {
if (it.isBlank()) return@forEachLine
@@ -135,4 +133,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 3f534842f..71c5bda4d 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/InputMethodPicker.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/InputMethodPicker.kt
@@ -14,6 +14,7 @@ import android.view.inputmethod.InputMethodSubtype
import helium314.keyboard.latin.LatinIME
import helium314.keyboard.latin.R
import helium314.keyboard.latin.RichInputMethodManager
+import helium314.keyboard.latin.utils.SubtypeLocaleUtils.displayName
// similar to what showSubtypePicker does in https://github.com/rkkr/simple-keyboard/blob/master/app/src/main/java/rkr/simplekeyboard/inputmethod/latin/RichInputMethodManager.java
fun createInputMethodPickerDialog(latinIme: LatinIME, richImm: RichInputMethodManager, windowToken: IBinder): AlertDialog {
@@ -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/IntentUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/IntentUtils.kt
new file mode 100644
index 000000000..da1b06bd8
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/latin/utils/IntentUtils.kt
@@ -0,0 +1,26 @@
+package helium314.keyboard.latin.utils
+
+import android.content.Context
+import android.content.Intent
+import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
+import helium314.keyboard.latin.inputlogic.InputLogic
+import helium314.keyboard.latin.utils.Log.i
+
+object IntentUtils {
+ val TAG: String = InputLogic::class.java.simpleName
+ private val ACTION_SEND_INTENT = "helium314.keyboard.latin.ACTION_SEND_INTENT"
+ private val EXTRA_NUMBER = "EXTRA_NUMBER"
+
+ @JvmStatic
+ fun handleSendIntentKey(context: Context, mKeyCode: Int) {
+ val intentNumber = (KeyCode.SEND_INTENT_ONE + 1) - mKeyCode;
+
+ val intent: Intent = Intent(ACTION_SEND_INTENT).apply {
+ putExtra(EXTRA_NUMBER, intentNumber)
+ }
+
+ context.sendBroadcast(intent)
+ i(TAG, "Sent broadcast for intent number: $intentNumber")
+ }
+}
+
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java
index 6bae92d5e..310ece153 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java
+++ b/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java
@@ -52,10 +52,18 @@ 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) {
@@ -90,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 68a99be99..13bcb24c0 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt
@@ -17,7 +17,7 @@ object LayoutUtils {
return context.assets.list(layoutType.folder)?.map { it.substringBefore(".") }.orEmpty()
if (locale == null)
return SubtypeSettings.getAllAvailableSubtypes()
- .mapTo(HashSet()) { it.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)
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 f66c76aba..ba0fca550 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/ScriptUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/ScriptUtils.kt
@@ -168,7 +168,7 @@ object ScriptUtils {
"sr", "mk", "ru", "uk", "mn", "be", "kk", "ky", "bg", "xdq", "cv", "mhr", "mns", "dru" -> SCRIPT_CYRILLIC
"ka" -> SCRIPT_GEORGIAN
"el" -> SCRIPT_GREEK
- "iw" -> 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 88a5d9450..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
@@ -154,7 +156,7 @@ object SubtypeSettings {
}
else subtype.toPref()
}.joinToString(Separators.SETS)
- prefs.edit().putString(key, new).apply()
+ editor.putString(key, new)
}
editor.apply()
if (Settings.readDefaultLayoutName(type, prefs) == from)
@@ -164,10 +166,10 @@ 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 =
@@ -187,7 +189,6 @@ object SubtypeSettings {
reloadSystemLocales(context)
loadResourceSubtypes(context.resources)
- removeInvalidCustomSubtypes(context)
loadAdditionalSubtypes(context.prefs())
loadEnabledSubtypes(context)
}
@@ -215,24 +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")
- // todo: now we have a qwerty fallback anyway, consider removing this method (makes bugs more obvious to users)
- prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, additionalSubtypes.filterNot { it in subtypesToRemove }.joinToString(Separators.SETS)).apply()
- }
-
private fun loadAdditionalSubtypes(prefs: SharedPreferences) {
additionalSubtypes.clear()
val additionalSubtypeString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
@@ -260,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)
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 95875bac2..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,9 +50,18 @@ 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)
@@ -100,8 +113,6 @@ object SubtypeUtilsAdditional {
}
private fun getNameResId(locale: Locale, mainLayoutName: String): Int {
- val nameId = SubtypeLocaleUtils.getSubtypeNameResId(locale, mainLayoutName)
- if (nameId != SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) return nameId
SubtypeSettings.getResourceSubtypesForLocale(locale).forEach {
if (it.mainLayoutName() == mainLayoutName) return it.nameResId
}
@@ -150,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/TextRange.java b/app/src/main/java/helium314/keyboard/latin/utils/TextRange.java
index 659e01a95..1a0e81fdf 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/TextRange.java
+++ b/app/src/main/java/helium314/keyboard/latin/utils/TextRange.java
@@ -7,9 +7,13 @@
package helium314.keyboard.latin.utils;
import android.text.Spanned;
+import android.text.TextUtils;
import android.text.style.SuggestionSpan;
+import androidx.annotation.NonNull;
+
import java.util.Arrays;
+import java.util.Objects;
/**
* Represents a range of text, relative to the current cursor position.
@@ -95,6 +99,28 @@ public final class TextRange {
return writeIndex == readIndex ? spans : Arrays.copyOfRange(spans, 0, writeIndex);
}
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof TextRange textRange)) return false;
+ return mWordAtCursorStartIndex == textRange.mWordAtCursorStartIndex
+ && mWordAtCursorEndIndex == textRange.mWordAtCursorEndIndex
+ && mCursorIndex == textRange.mCursorIndex
+ && mHasUrlSpans == textRange.mHasUrlSpans
+ && TextUtils.equals(mTextAtCursor, textRange.mTextAtCursor)
+ && TextUtils.equals(mWord, textRange.mWord);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTextAtCursor, mWordAtCursorStartIndex, mWordAtCursorEndIndex, mCursorIndex, mWord, mHasUrlSpans);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return mTextAtCursor + ", " + mWord + ", " + mCursorIndex;
+ }
+
public TextRange(final CharSequence textAtCursor, final int wordAtCursorStartIndex,
final int wordAtCursorEndIndex, final int cursorIndex, final boolean hasUrlSpans) {
if (wordAtCursorStartIndex < 0 || cursorIndex < wordAtCursorStartIndex
@@ -109,4 +135,4 @@ public final class TextRange {
mHasUrlSpans = hasUrlSpans;
mWord = mTextAtCursor.subSequence(mWordAtCursorStartIndex, mWordAtCursorEndIndex);
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt
index 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/Icons.kt b/app/src/main/java/helium314/keyboard/settings/Icons.kt
index 676af9aa1..4bd04bbe6 100644
--- a/app/src/main/java/helium314/keyboard/settings/Icons.kt
+++ b/app/src/main/java/helium314/keyboard/settings/Icons.kt
@@ -11,14 +11,19 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.LayoutDirection
import helium314.keyboard.latin.R
@Composable
fun NextScreenIcon() {
- Icon(painterResource(R.drawable.ic_arrow_left), null, Modifier.scale(-1f, 1f))
+ Icon(
+ painterResource(R.drawable.ic_arrow_left), null,
+ if (LocalLayoutDirection.current == LayoutDirection.Ltr) Modifier.scale(-1f, 1f) else Modifier
+ )
}
@Composable
diff --git a/app/src/main/java/helium314/keyboard/settings/Misc.kt b/app/src/main/java/helium314/keyboard/settings/Misc.kt
index 2ca4add2c..fffc7ba1f 100644
--- a/app/src/main/java/helium314/keyboard/settings/Misc.kt
+++ b/app/src/main/java/helium314/keyboard/settings/Misc.kt
@@ -8,6 +8,9 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.DropdownMenu
@@ -43,6 +46,25 @@ fun WithSmallTitle(
}
}
+@Composable
+fun ActionRow(
+ modifier: Modifier = Modifier,
+ onClick: (() -> Unit)? = null,
+ verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+ content: @Composable RowScope.() -> Unit
+) {
+ val clickableModifier = if (onClick != null) Modifier.clickable(onClick = onClick)
+ else Modifier
+ Row(
+ modifier = modifier
+ .then(clickableModifier)
+ .fillMaxWidth()
+ .heightIn(min = 44.dp),
+ verticalAlignment = verticalAlignment,
+ content = content
+ )
+}
+
/** Icon if resource is a vector image, (bitmap) Image otherwise */
@Composable
fun IconOrImage(@DrawableRes resId: Int, name: String?, sizeDp: Float) {
diff --git a/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt b/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
index 8fd287bf7..16b2e2d92 100644
--- a/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
+++ b/app/src/main/java/helium314/keyboard/settings/SearchScreen.kt
@@ -6,12 +6,17 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@@ -20,6 +25,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
@@ -40,6 +46,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import helium314.keyboard.latin.R
@@ -58,36 +65,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) },
@@ -111,93 +124,90 @@ fun SearchScreen(
// keyboard in unexpected situations such as going back from another screen, which is rather annoying
var searchText by remember { mutableStateOf(TextFieldValue()) }
var showSearch by remember { mutableStateOf(false) }
- Column(Modifier.fillMaxSize()) {
+ 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 = {
- if (icon == null)
- IconButton(onClick = { setShowSearch(!showSearch) }) { SearchIcon() }
- else
- icon()
- if (menu != null)
- Box {
- var showMenu by remember { mutableStateOf(false) }
- IconButton(
- onClick = { showMenu = true }
- ) { Icon(painterResource(R.drawable.ic_arrow_left), "menu", Modifier.rotate(-90f)) }
- DropdownMenu(
- expanded = showMenu,
- onDismissRequest = { showMenu = false }
- ) {
- menu.forEach {
- DropdownMenuItem(
- text = { Text(it.first) },
- onClick = { showMenu = false; it.second() }
- )
+ fun setShowSearch(value: Boolean) {
+ showSearch = value
+ if (!value) searchText = TextFieldValue()
+ }
+ BackHandler {
+ if (showSearch || searchText.text.isNotEmpty()) setShowSearch(false)
+ else onClickBack()
+ }
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ ) {
+ Column {
+ TopAppBar(
+ title = title,
+ windowInsets = 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)
+ }
+ }
}
}
}
@@ -233,7 +243,8 @@ fun ExpandableSearchField(
}) { CloseIcon(android.R.string.cancel) } },
singleLine = true,
colors = colors,
- textStyle = contentTextDirectionStyle
+ textStyle = contentTextDirectionStyle,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search)
)
}
}
diff --git a/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt b/app/src/main/java/helium314/keyboard/settings/SettingsActivity.kt
index acb74220a..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
@@ -59,6 +68,7 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
private val crashReportFiles = MutableStateFlow>(emptyList())
private var paused = true
+ @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Settings.getValues() == null) {
@@ -66,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
@@ -98,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 },
@@ -111,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)
- }
}
}
}
@@ -143,6 +156,8 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
}
intent = null
}
+
+ enableEdgeToEdge()
}
override fun onStart() {
@@ -175,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 {
@@ -206,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
@@ -231,9 +232,6 @@ class SettingsActivity : ComponentActivity(), SharedPreferences.OnSharedPreferen
var forceNight: Boolean? = null
var forceTheme: String? = null
-
- // weird inset forwarding because otherwise layout dialog sometimes doesn't care about keyboard showing
- var bottomInsets = MutableStateFlow(0)
}
override fun onSharedPreferenceChanged(prefereces: SharedPreferences?, key: String?) {
diff --git a/app/src/main/java/helium314/keyboard/settings/Theme.kt b/app/src/main/java/helium314/keyboard/settings/Theme.kt
index 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/ColorThemePickerDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt
index 6c1f84893..24101e86e 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/ColorThemePickerDialog.kt
@@ -272,7 +272,8 @@ private fun loadColorString(colorString: String, prefs: SharedPreferences): Bool
try {
allColors[ColorType.valueOf(it.key)] = it.value
} catch (_: IllegalArgumentException) {
- themeName = decodeBase36(it.key)
+ if (it.value == 0) // hacky way of storing theme name: put it in a key with value 0
+ runCatching { decodeBase36(it.key) }.getOrNull()?.let { themeName = it }
}
}
themeName = KeyboardTheme.getUnusedThemeName(themeName, prefs)
diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
index 7cbc5e942..516ac5217 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/DictionaryDialog.kt
@@ -3,11 +3,8 @@ package helium314.keyboard.settings.dialogs
import android.content.Intent
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -16,8 +13,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -28,9 +23,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -45,11 +38,11 @@ import helium314.keyboard.settings.DeleteButton
import helium314.keyboard.settings.ExpandButton
import helium314.keyboard.settings.Theme
import helium314.keyboard.settings.dictionaryFilePicker
-import helium314.keyboard.settings.preferences.PreferenceCategory
import helium314.keyboard.settings.previewDark
import helium314.keyboard.settings.screens.getUserAndInternalDictionaries
import java.io.File
import java.util.Locale
+import androidx.compose.ui.platform.LocalConfiguration
@Composable
fun DictionaryDialog(
@@ -66,24 +59,39 @@ fun DictionaryDialog(
onConfirmed = {},
confirmButtonText = null,
cancelButtonText = stringResource(R.string.dialog_close),
- title = { Text(locale.localizedDisplayName(ctx)) },
+ title = { Text(locale.localizedDisplayName(ctx.resources)) },
content = {
val state = rememberScrollState()
Column(Modifier.verticalScroll(state)) {
if (hasInternal) {
- val color = if (mainDict == null) MaterialTheme.colorScheme.onSurface
+ val color = if (mainDict == null) MaterialTheme.typography.titleSmall.color
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) // for disabled look
- Text(stringResource(R.string.internal_dictionary_summary), color = color, modifier = Modifier.fillMaxWidth())
+ val bottomPadding = if (mainDict == null) 12.dp else 0.dp
+ Text(stringResource(R.string.internal_dictionary_summary),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = bottomPadding),
+ color = color,
+ style = MaterialTheme.typography.titleSmall
+ )
}
if (mainDict != null)
DictionaryDetails(mainDict)
if (addonDicts.isNotEmpty()) {
- PreferenceCategory(stringResource(R.string.dictionary_settings_title))
+ HorizontalDivider()
+ Text(stringResource(R.string.dictionary_category_title),
+ modifier = Modifier.padding(vertical = 12.dp),
+ style = MaterialTheme.typography.titleSmall
+ )
addonDicts.forEach { DictionaryDetails(it) }
}
val dictString = createDictionaryTextAnnotated(locale)
if (dictString.isNotEmpty()) {
HorizontalDivider()
+ Text(stringResource(R.string.dictionary_available),
+ modifier = Modifier.padding(top = 12.dp, bottom = 4.dp),
+ style = MaterialTheme.typography.titleSmall
+ )
Text(dictString, style = LocalTextStyle.current.merge(lineHeight = 1.8.em))
}
}
@@ -115,11 +123,12 @@ private fun DictionaryDetails(dict: File) {
DeleteButton { showDeleteDialog = true }
ExpandButton { showDetails = !showDetails }
}
- AnimatedVisibility(showDetails, enter = fadeIn(), exit = fadeOut()) { // default animation looks better, but makes the dialog flash
+ // default animations look better but make the dialog flash, see also MultiSliderPreference
+ AnimatedVisibility(showDetails, enter = fadeIn(), exit = fadeOut()) {
Text(
- header.info(LocalContext.current.resources.configuration.locale()),
+ header.info(LocalConfiguration.current.locale()),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(horizontal = 10.dp)
+ modifier = Modifier.padding(start = 10.dp, top = 0.dp, end = 10.dp, bottom = 12.dp)
)
}
if (showDeleteDialog)
diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutEditDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutEditDialog.kt
index 81a3c0451..aae610370 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutEditDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutEditDialog.kt
@@ -2,11 +2,15 @@
package helium314.keyboard.settings.dialogs
import android.widget.Toast
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.exclude
+import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
@@ -14,10 +18,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.DialogProperties
import helium314.keyboard.keyboard.KeyboardSwitcher
import helium314.keyboard.latin.R
import helium314.keyboard.latin.utils.LayoutType
@@ -50,7 +55,6 @@ fun LayoutEditDialog(
val ctx = LocalContext.current
val scope = rememberCoroutineScope()
val startIsCustom = LayoutUtilsCustom.isCustomLayout(initialLayoutName)
- val bottomInsets by SettingsActivity.bottomInsets.collectAsState()
var displayNameValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(
if (startIsCustom) LayoutUtilsCustom.getDisplayName(initialLayoutName)
@@ -110,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 e42631bb7..ace5aafb8 100644
--- a/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutPickerDialog.kt
+++ b/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutPickerDialog.kt
@@ -204,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..a82a07217 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(
@@ -46,16 +47,16 @@ fun NewDictionaryDialog(
} else if (header != null) {
val ctx = LocalContext.current
val dictLocale = header.mLocaleString.constructLocale()
- var locale by remember { mutableStateOf(mainLocale ?: dictLocale) }
val enabledLanguages = SubtypeSettings.getEnabledSubtypes().map { it.locale().language }
val comparer = compareBy({ it != mainLocale }, { it != dictLocale }, { it.language !in enabledLanguages }, { it.script() != dictLocale.script() })
val locales = SubtypeSettings.getAvailableSubtypeLocales()
.filter { it.script() == dictLocale.script() || it.script() == mainLocale?.script() }
.sortedWith(comparer)
- val cacheDir = DictionaryInfoUtils.getAndCreateCacheDirectoryForLocale(locale, ctx)
+ var locale by remember { mutableStateOf(mainLocale ?: dictLocale.takeIf { it in locales } ?: locales.first()) }
+ 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/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 ee771d5bf..159d79671 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(),
) }
@@ -66,7 +67,7 @@ fun BackupRestorePreference(setting: Setting) {
val files = mutableListOf()
filesDir.walk().forEach { file ->
val path = file.path.replace(filesPath, "")
- if (backupFilePatterns.any { path.matches(it) })
+ if (file.isFile && backupFilePatterns.any { path.matches(it) })
files.add(file)
}
val protectedFilesDir = DeviceProtectedUtils.getFilesDir(ctx)
@@ -74,7 +75,7 @@ fun BackupRestorePreference(setting: Setting) {
val protectedFiles = mutableListOf()
protectedFilesDir.walk().forEach { file ->
val path = file.path.replace(protectedFilesPath, "")
- if (backupFilePatterns.any { path.matches(it) })
+ if (file.isFile && backupFilePatterns.any { path.matches(it) })
protectedFiles.add(file)
}
val wait = CountDownLatch(1)
@@ -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()
+ 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/MultiSliderPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/MultiSliderPreference.kt
new file mode 100644
index 000000000..e991344c9
--- /dev/null
+++ b/app/src/main/java/helium314/keyboard/settings/preferences/MultiSliderPreference.kt
@@ -0,0 +1,205 @@
+// SPDX-License-Identifier: GPL-3.0-only
+package helium314.keyboard.settings.preferences
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import helium314.keyboard.latin.R
+import helium314.keyboard.latin.settings.createPrefKeyForBooleanSettings
+import helium314.keyboard.latin.utils.prefs
+import helium314.keyboard.settings.Theme
+import helium314.keyboard.settings.WithSmallTitle
+import helium314.keyboard.settings.dialogs.ThreeButtonAlertDialog
+import helium314.keyboard.settings.previewDark
+
+// too specialized for using a more generic dialog
+// actual key for each setting is baseKey with one _true/_false appended per dimension (need to keep order!)
+// todo: possible adjustments, maybe depending on user feedback
+// should dimension checkboxes have any other effect than just showing / hiding sliders?
+// one could argue that e.g. when disabling the split checkbox, then split mode should not affect the setting
+// store checkbox states?
+// if so, per setting or global?
+// show a description? currently commented because it could get long, even without showing the variations
+// maybe if we store the checkbox state in a setting, we could use it for determining what to show
+@Composable
+fun MultiSliderPreference(
+ name: String,
+ baseKey: String,
+ dimensions: List,
+ defaults: Array,
+ range: ClosedFloatingPointRange,
+ description: (Float) -> String,
+ onDone: () -> Unit
+) {
+ if (defaults.size != 1.shl(dimensions.size))
+ throw ArithmeticException("defaults size does not match with dimensions, expected ${1.shl(dimensions.size)}, got ${defaults.size}")
+ var showDialog by remember { mutableStateOf(false) }
+ //val (_, keys) = remember { createVariantsAndKeys(dimensions, baseKey) }
+ //val prefs = LocalContext.current.prefs()
+ Preference(
+ name = name,
+ onClick = { showDialog = true },
+ //description = keys.mapIndexed { i, it -> description(prefs.getFloat(it, defaults[i])) }.joinToString(" $SPLIT ")
+ )
+ if (showDialog)
+ MultiSliderDialog(
+ onDismissRequest = { showDialog = false },
+ title = { Text(name) },
+ baseKey = baseKey,
+ onDone = onDone,
+ defaultValues = defaults,
+ range = range,
+ dimensions = dimensions,
+ positionString = description
+ )
+}
+
+// SliderDialog, but for multiple sliders with same range, each with a different setting and title
+@Composable
+private fun MultiSliderDialog(
+ onDismissRequest: () -> Unit,
+ title: @Composable () -> Unit,
+ baseKey: String,
+ onDone: () -> Unit,
+ defaultValues: Array,
+ range: ClosedFloatingPointRange,
+ dimensions: List,
+ modifier: Modifier = Modifier,
+ positionString: (Float) -> String,
+) {
+ val (variants, keys) = createVariantsAndKeys(dimensions, baseKey)
+ var checked by remember { mutableStateOf(List(variants.size) { true }) }
+ val prefs = LocalContext.current.prefs()
+ val done = remember { mutableMapOf Unit>() }
+
+ ThreeButtonAlertDialog(
+ onDismissRequest = onDismissRequest,
+ onConfirmed = { done.values.forEach { it.invoke() }; onDone() },
+ modifier = modifier,
+ title = title,
+ content = {
+ CompositionLocalProvider(
+ LocalTextStyle provides MaterialTheme.typography.bodyLarge
+ ) {
+ val state = rememberScrollState()
+ Column(Modifier.verticalScroll(state)) {
+ if (dimensions.size > 1) {
+ dimensions.forEachIndexed { i, dimension ->
+ DimensionCheckbox(checked[i], dimension) {
+ checked = checked.mapIndexed { j, c -> if (i == j) it else c }
+ }
+ }
+ }
+ variants.forEachIndexed { i, variant ->
+ val key = keys[i]
+ var sliderPosition by remember { mutableFloatStateOf(prefs.getFloat(key, defaultValues[i])) }
+ if (!done.contains(variant))
+ done[variant] = {
+ if (sliderPosition == defaultValues[i])
+ prefs.edit().remove(key).apply()
+ else
+ prefs.edit().putFloat(key, sliderPosition).apply()
+ }
+ val forbiddenDimensions = dimensions.filterIndexed { index, _ -> !checked[index] }
+ val visible = variant.split(SPLIT).none { it in forbiddenDimensions }
+ // default animations make the dialog flash (see also DictionaryDialog)
+ AnimatedVisibility(visible, exit = fadeOut(), enter = fadeIn()) {
+ WithSmallTitle(variant.ifEmpty { stringResource(R.string.button_default) }) {
+ Slider(
+ value = sliderPosition,
+ onValueChange = { sliderPosition = it },
+ valueRange = range,
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(positionString(sliderPosition))
+ TextButton({ sliderPosition = defaultValues[i] }) { Text(stringResource(R.string.button_default)) }
+ }
+ Spacer(Modifier.height(6.dp))
+ }
+ }
+ }
+ }
+ }
+ },
+ )
+}
+
+@Composable
+private fun DimensionCheckbox(checked: Boolean, dimension: String, onCheckedChange: (Boolean) -> Unit) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth().clickable { onCheckedChange(!checked) }
+ ) {
+ Checkbox(
+ checked = checked,
+ onCheckedChange = { onCheckedChange(it) }
+ )
+ Text(dimension)
+ }
+}
+
+private fun createVariantsAndKeys(dimensions: List, baseKey: String): Pair, List> {
+ val variants = mutableListOf("")
+ val keys = mutableListOf(createPrefKeyForBooleanSettings(baseKey, 0, dimensions.size))
+ var i = 1
+ dimensions.forEach { dimension ->
+ variants.toList().forEach { variant ->
+ if (variant.isEmpty()) variants.add(dimension)
+ else variants.add(variant + SPLIT + dimension)
+ keys.add(createPrefKeyForBooleanSettings(baseKey, i, dimensions.size))
+ i++
+ }
+ }
+ return variants to keys
+}
+
+private const val SPLIT = " / "
+
+@Preview
+@Composable
+private fun Preview() {
+ Theme(previewDark) {
+ MultiSliderDialog(
+ onDismissRequest = { },
+ onDone = { },
+ positionString = { "${it.toInt()}%"},
+ defaultValues = Array(8) { 100f - it % 2 * 50f },
+ range = 0f..500f,
+ title = { Text("bottom padding scale") },
+ dimensions = listOf("landscape", "unfolded", "split"),
+ baseKey = ""
+ )
+ }
+}
diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt
index 8a52a914d..97138672a 100644
--- a/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt
+++ b/app/src/main/java/helium314/keyboard/settings/preferences/SliderPreference.kt
@@ -27,6 +27,7 @@ fun SliderPreference(
range: ClosedFloatingPointRange