diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/LayoutParser.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/LayoutParser.kt index 3164cc698..d2b51fb92 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/LayoutParser.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/LayoutParser.kt @@ -59,8 +59,7 @@ object LayoutParser { /** Parse simple layouts, defined only as rows of (normal) keys with popup keys. */ fun parseSimpleString(layoutText: String): List> { - val rowStrings = layoutText.replace("\r\n", "\n").split("\\n\\s*\\n".toRegex()).filter { it.isNotBlank() } - return rowStrings.map { row -> + return LayoutUtils.getSimpleRowStrings(layoutText).map { row -> row.split("\n").mapNotNull { parseKey(it) } } } diff --git a/app/src/main/java/helium314/keyboard/latin/App.kt b/app/src/main/java/helium314/keyboard/latin/App.kt index 23308a14a..6172dac58 100644 --- a/app/src/main/java/helium314/keyboard/latin/App.kt +++ b/app/src/main/java/helium314/keyboard/latin/App.kt @@ -161,7 +161,7 @@ fun checkVersionUpgrade(context: Context) { split[1] = newName split.joinToString(":") } - Settings.writePrefAdditionalSubtypes(prefs, newSubtypeStrings.joinToString(";")) + prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, newSubtypeStrings.joinToString(";")).apply() } // rename other custom layouts LayoutUtilsCustom.onLayoutFileChanged() @@ -630,7 +630,7 @@ private fun upgradesWhenComingFromOldAppName(context: Context) { val localeString = it.substringBefore(":") additionalSubtypes.add(it.replace(localeString, localeString.constructLocale().toLanguageTag())) } - Settings.writePrefAdditionalSubtypes(prefs, additionalSubtypes.joinToString(";")) + prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, additionalSubtypes.joinToString(";")).apply() } // move pinned clips to credential protected storage if device is not locked (should never happen) if (!prefs.contains(Settings.PREF_PINNED_CLIPS)) return diff --git a/app/src/main/java/helium314/keyboard/latin/LatinIME.java b/app/src/main/java/helium314/keyboard/latin/LatinIME.java index d2e75a457..f18c530e7 100644 --- a/app/src/main/java/helium314/keyboard/latin/LatinIME.java +++ b/app/src/main/java/helium314/keyboard/latin/LatinIME.java @@ -523,6 +523,11 @@ public class LatinIME extends InputMethodService implements } final class SubtypeState { + // When HintLocales causes a subtype override, we store + // the overridden subtype here in order to restore it when + // we switch to another input context that has no HintLocales. + private InputMethodSubtype mOverriddenByLocale; + private InputMethodSubtype mLastActiveSubtype; private boolean mCurrentSubtypeHasBeenUsed = true; // starting with true avoids immediate switch @@ -530,6 +535,70 @@ public class LatinIME extends InputMethodService implements mCurrentSubtypeHasBeenUsed = true; } + // TextFields can provide locale/language hints that the IME should use via 'hintLocales'. + // If a matching subtype is found, we temporarily switch to that subtype until + // we return to a context that does not provide any hints, or until the user + // explicitly changes the language/subtype in use. + public InputMethodSubtype getSubtypeForLocales(final RichInputMethodManager richImm, final Iterable locales) { + final InputMethodSubtype overriddenByLocale = mOverriddenByLocale; + if (locales == null) { + if (overriddenByLocale != null) { + // no locales provided, so switch back to + // whatever subtype was used last time. + mOverriddenByLocale = null; + + return overriddenByLocale; + } + + return null; + } + + final InputMethodSubtype currentSubtype = richImm.getCurrentSubtype().getRawSubtype(); + final Locale currentSubtypeLocale = richImm.getCurrentSubtypeLocale(); + final int minimumMatchLevel = 3; // LocaleUtils.LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; + + // Try finding a subtype matching the hint language. + for (final Locale hintLocale : locales) { + if (LocaleUtils.INSTANCE.getMatchLevel(hintLocale, currentSubtypeLocale) >= minimumMatchLevel + || CollectionsKt.any(mSettings.getCurrent().mSecondaryLocales, + (secLocale) -> LocaleUtils.INSTANCE.getMatchLevel(hintLocale, secLocale) >= minimumMatchLevel)) { + // current locales are already a good match, and we want to avoid unnecessary layout switches. + return null; + } + + final InputMethodSubtype subtypeForHintLocale = richImm.findSubtypeForHintLocale(hintLocale); + if (subtypeForHintLocale == null) { + continue; + } + + if (subtypeForHintLocale.equals(currentSubtype)) { + // no need to switch, we already use the correct locale. + return null; + } + + if (overriddenByLocale == null) { + // auto-switching based on hint locale, so store + // whatever subtype was in use so we can switch back + // to it later when there are no hint locales. + mOverriddenByLocale = currentSubtype; + } + + return subtypeForHintLocale; + } + + return null; + } + + public void onSubtypeChanged(final InputMethodSubtype oldSubtype, + final InputMethodSubtype newSubtype) { + if (oldSubtype != mOverriddenByLocale) { + // Whenever the subtype is changed, clear tracking + // the subtype that is overridden by a HintLocale as + // we no longer have a subtype to automatically switch back to. + mOverriddenByLocale = null; + } + } + public void switchSubtype(final RichInputMethodManager richImm) { final InputMethodSubtype currentSubtype = richImm.getCurrentSubtype().getRawSubtype(); final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype; @@ -858,6 +927,8 @@ public class LatinIME extends InputMethodService implements return; } InputMethodSubtype oldSubtype = mRichImm.getCurrentSubtype().getRawSubtype(); + + mSubtypeState.onSubtypeChanged(oldSubtype, subtype); StatsUtils.onSubtypeChanged(oldSubtype, subtype); mRichImm.onSubtypeChanged(subtype); mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype), @@ -876,20 +947,10 @@ public class LatinIME extends InputMethodService implements super.onStartInput(editorInfo, restarting); final List hintLocales = EditorInfoCompatUtils.getHintLocales(editorInfo); - if (hintLocales == null) { - return; - } - // Try switching to a subtype matching the hint language. - for (final Locale hintLocale : hintLocales) { - if (LocaleUtils.INSTANCE.getMatchLevel(hintLocale, mRichImm.getCurrentSubtypeLocale()) >= 3 - || CollectionsKt.any(mSettings.getCurrent().mSecondaryLocales, (secLocale) -> LocaleUtils.INSTANCE.getMatchLevel(hintLocale, secLocale) >= 3)) - return; // current locales are already a good match, and we want to avoid unnecessary layout switches - final InputMethodSubtype newSubtype = mRichImm.findSubtypeForHintLocale(hintLocale); - if (newSubtype == null) continue; - if (newSubtype.equals(mRichImm.getCurrentSubtype().getRawSubtype())) - return; // no need to switch, we already use the correct locale - mHandler.postSwitchLanguage(newSubtype); - break; + final InputMethodSubtype subtypeForLocales = mSubtypeState.getSubtypeForLocales(mRichImm, hintLocales); + if (subtypeForLocales != null) { + // found a better subtype using hint locales that we should switch to. + mHandler.postSwitchLanguage(subtypeForLocales); } } diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt index 0301409ba..689087f9f 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -9,7 +9,6 @@ import helium314.keyboard.keyboard.KeyboardTheme import helium314.keyboard.latin.BuildConfig import helium314.keyboard.latin.common.Constants.Separators import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue -import helium314.keyboard.latin.utils.JniUtils import helium314.keyboard.latin.utils.LayoutType import helium314.keyboard.latin.utils.POPUP_KEYS_LABEL_DEFAULT import helium314.keyboard.latin.utils.POPUP_KEYS_ORDER_DEFAULT @@ -74,9 +73,9 @@ object Defaults { const val PREF_LANGUAGE_SWITCH_KEY = "internal" const val PREF_SHOW_EMOJI_KEY = false const val PREF_VARIABLE_TOOLBAR_DIRECTION = true - const val PREF_ADDITIONAL_SUBTYPES = "de${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=qwerty${Separators.SETS}" + - "fr${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=qwertz${Separators.SETS}" + - "hu${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=qwerty" + const val PREF_ADDITIONAL_SUBTYPES = "de${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwerty${Separators.SETS}" + + "fr${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwertz${Separators.SETS}" + + "hu${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwerty" const val PREF_ENABLE_SPLIT_KEYBOARD = false const val PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE = false const val PREF_SPLIT_SPACER_SCALE = SettingsValues.DEFAULT_SIZE_SCALE @@ -152,8 +151,6 @@ object Defaults { const val PREF_EMOJI_RECENT_KEYS = "" const val PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID = 0 const val PREF_PINNED_CLIPS = "" - @JvmField - val PREF_LIBRARY_CHECKSUM: String = JniUtils.expectedDefaultChecksum() const val PREF_SHOW_DEBUG_SETTINGS = false val PREF_DEBUG_MODE = BuildConfig.DEBUG const val PREF_SHOW_SUGGESTION_INFOS = false diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java index 89e93e8dd..1510a65be 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.java +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.java @@ -312,10 +312,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang mPrefs.edit().putBoolean(Settings.PREF_ALWAYS_INCOGNITO_MODE, !oldValue).apply(); } - public static void writePrefAdditionalSubtypes(final SharedPreferences prefs, final String prefSubtypes) { - prefs.edit().putString(PREF_ADDITIONAL_SUBTYPES, prefSubtypes).apply(); - } - public static 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; diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsSubtype.kt b/app/src/main/java/helium314/keyboard/latin/settings/SettingsSubtype.kt index a5745295b..ca18b7fda 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsSubtype.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsSubtype.kt @@ -11,11 +11,9 @@ import helium314.keyboard.latin.common.LocaleUtils.constructLocale import helium314.keyboard.latin.define.DebugFlags import helium314.keyboard.latin.utils.LayoutType import helium314.keyboard.latin.utils.LayoutType.Companion.toExtraValue -import helium314.keyboard.latin.utils.LayoutUtilsCustom import helium314.keyboard.latin.utils.Log import helium314.keyboard.latin.utils.ScriptUtils import helium314.keyboard.latin.utils.ScriptUtils.script -import helium314.keyboard.latin.utils.SubtypeLocaleUtils import helium314.keyboard.latin.utils.SubtypeUtilsAdditional import helium314.keyboard.latin.utils.locale import java.util.Locale @@ -27,23 +25,9 @@ data class SettingsSubtype(val locale: Locale, val extraValues: String) { /** Creates an additional subtype from the SettingsSubtype. * Resulting InputMethodSubtypes are equal if SettingsSubtypes are equal */ - fun toAdditionalSubtype(): InputMethodSubtype? { + fun toAdditionalSubtype(): InputMethodSubtype { val asciiCapable = locale.script() == ScriptUtils.SCRIPT_LATIN - val subtype = SubtypeUtilsAdditional.createAdditionalSubtype(locale, extraValues, asciiCapable, true) - - // todo: this is returns null for all non-latin layouts - // either fix it, or remove the check - // if removed, removing a layout will result in fallback qwerty even for non-ascii, but this is better than the current alternative -/* if (subtype.nameResId == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT - && mainLayoutName()?.endsWith("+") != true // "+" layouts and custom layouts are always "unknown" - && !LayoutUtilsCustom.isCustomLayout(mainLayoutName() ?: SubtypeLocaleUtils.QWERTY) - ) { - // Skip unknown keyboard layout subtype. This may happen when predefined keyboard - // layout has been removed. - Log.w(SettingsSubtype::class.simpleName, "unknown additional subtype $this") - return null - }*/ - return subtype + return SubtypeUtilsAdditional.createAdditionalSubtype(locale, extraValues, asciiCapable, true) } fun mainLayoutName() = LayoutType.getMainLayoutFromExtraValue(extraValues) @@ -54,7 +38,7 @@ data class SettingsSubtype(val locale: Locale, val extraValues: String) { val newList = extraValues.split(",") .filterNot { it.isBlank() || it.startsWith("$extraValueKey=") || it == extraValueKey } val newValue = if (extraValue == null) extraValueKey else "$extraValueKey=$extraValue" - val newValues = (newList + newValue).joinToString(",") + val newValues = (newList + newValue).sorted().joinToString(",") return copy(extraValues = newValues) } diff --git a/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java index 603e32ab6..6bae92d5e 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java +++ b/app/src/main/java/helium314/keyboard/latin/utils/JniUtils.java @@ -13,7 +13,6 @@ import android.text.TextUtils; import helium314.keyboard.latin.App; import helium314.keyboard.latin.BuildConfig; -import helium314.keyboard.latin.settings.Defaults; import helium314.keyboard.latin.settings.Settings; import java.io.File; @@ -63,7 +62,7 @@ public final class JniUtils { // we want the default preferences, because storing the checksum in device protected storage is discouraged // see https://developer.android.com/reference/android/content/Context#createDeviceProtectedStorageContext() // if device is locked, this will throw an IllegalStateException - wantedChecksum = KtxKt.protectedPrefs(app).getString(Settings.PREF_LIBRARY_CHECKSUM, Defaults.PREF_LIBRARY_CHECKSUM); + wantedChecksum = KtxKt.protectedPrefs(app).getString(Settings.PREF_LIBRARY_CHECKSUM, expectedDefaultChecksum()); } final FileInputStream libStream = new FileInputStream(userSuppliedLibrary); final String checksum = ChecksumCalculator.INSTANCE.checksum(libStream); diff --git a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt index 56afe9ce9..68a99be99 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtils.kt @@ -1,6 +1,8 @@ package helium314.keyboard.latin.utils import android.content.Context +import helium314.keyboard.keyboard.internal.keyboard_parser.floris.SimplePopups +import helium314.keyboard.keyboard.internal.keyboard_parser.getOrCreate import helium314.keyboard.latin.R import helium314.keyboard.latin.settings.Defaults.default import helium314.keyboard.latin.utils.LayoutType.Companion.folder @@ -26,6 +28,7 @@ object LayoutUtils { fun getLMainLayoutsForLocales(locales: List, context: Context): Collection = locales.flatMapTo(HashSet()) { getAvailableLayouts(LayoutType.MAIN, context, it) }.sorted() + /** gets content for built-in (non-custom) layout [layoutName], with fallback to qwerty */ fun getContent(layoutType: LayoutType, layoutName: String, context: Context): String { val layouts = context.assets.list(layoutType.folder)!! layouts.firstOrNull { it.startsWith("$layoutName.") } @@ -33,4 +36,27 @@ object LayoutUtils { val fallback = layouts.first { it.startsWith(layoutType.default) } // must exist! return context.assets.open(layoutType.folder + File.separator + fallback).reader().readText() } + + fun getContentWithPlus(mainLayoutName: String, locale: Locale, context: Context): String { + val content = getContent(LayoutType.MAIN, mainLayoutName, context) + if (!mainLayoutName.endsWith("+")) + return content + // the stuff below will not work if we add "+" layouts in json format + // ideally we should serialize keyData to json to solve this + val rows = getSimpleRowStrings(content) + val localeKeyboardInfos = getOrCreate(context, locale) + return rows.mapIndexed { i, row -> + val extraKeys = localeKeyboardInfos.getExtraKeys(i + 1) ?: return@mapIndexed row + val rowList = row.split("\n").filterNot { it.isEmpty() }.toMutableList() + extraKeys.forEach { key -> + val popups = (key.popup as? SimplePopups)?.popupKeys?.joinToString(" ") + ?.takeIf { it.isNotEmpty() }?.let { " $it" } ?: "" + rowList.add(key.label + popups) + } + rowList.joinToString("\n") + }.joinToString("\n\n") + } + + fun getSimpleRowStrings(layoutContent: String): List = + layoutContent.replace("\r\n", "\n").split("\\n\\s*\\n".toRegex()).filter { it.isNotBlank() } } diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt index 18739953b..f533223ec 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt @@ -52,9 +52,8 @@ object SubtypeSettings { fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) { val subtype = newSubtype.toSettingsSubtype() - val subtypes = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!! - .split(Separators.SETS).filter { it.isNotBlank() }.map { it.toSettingsSubtype() } + subtype - val newString = subtypes.map { it.toPref() }.toSortedSet().joinToString(Separators.SETS) + val subtypes = createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!) + subtype + val newString = createPrefSubtypes(subtypes) prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newString) } if (newSubtype !in enabledSubtypes) { @@ -74,10 +73,8 @@ object SubtypeSettings { fun getSelectedSubtype(prefs: SharedPreferences): InputMethodSubtype { val selectedSubtype = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype() - if (selectedSubtype.isAdditionalSubtype(prefs)) { - val selectedAdditionalSubtype = selectedSubtype.toAdditionalSubtype() - if (selectedAdditionalSubtype != null) return selectedAdditionalSubtype - } + if (selectedSubtype.isAdditionalSubtype(prefs)) + return selectedSubtype.toAdditionalSubtype() // no additional subtype, must be a resource subtype val subtype = enabledSubtypes.firstOrNull { it.toSettingsSubtype() == selectedSubtype } @@ -157,6 +154,15 @@ object SubtypeSettings { RichInputMethodManager.getInstance().refreshSubtypeCaches() } + fun createSettingsSubtypes(prefSubtypes: String): List = + prefSubtypes.split(Separators.SETS).mapNotNull { + if (it.isEmpty()) null + else it.toSettingsSubtype() + } + + fun createPrefSubtypes(subtypes: Collection): String = + subtypes.map { it.toPref() }.toSortedSet().joinToString(Separators.SETS) + fun init(context: Context) { SubtypeLocaleUtils.init(context) // necessary to get the correct getKeyboardLayoutSetName @@ -207,7 +213,8 @@ object SubtypeSettings { } if (subtypesToRemove.isEmpty()) return Log.w(TAG, "removing custom subtypes without main layout files: $subtypesToRemove") - Settings.writePrefAdditionalSubtypes(prefs, additionalSubtypes.filterNot { it in subtypesToRemove }.joinToString(Separators.SETS)) + // 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) { @@ -220,15 +227,11 @@ object SubtypeSettings { // requires loadResourceSubtypes to be called before private fun loadEnabledSubtypes(context: Context) { val prefs = context.prefs() - val settingsSubtypes = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!! - .split(Separators.SETS).filter { it.isNotEmpty() }.map { it.toSettingsSubtype() } + val settingsSubtypes = createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!) for (settingsSubtype in settingsSubtypes) { if (settingsSubtype.isAdditionalSubtype(prefs)) { - val additionalSubtype = settingsSubtype.toAdditionalSubtype() - if (additionalSubtype != null) { - enabledSubtypes.add(additionalSubtype) - continue - } + enabledSubtypes.add(settingsSubtype.toAdditionalSubtype()) + continue } val subtypesForLocale = resourceSubtypesByLocale[settingsSubtype.locale] if (subtypesForLocale == null) { @@ -258,12 +261,11 @@ object SubtypeSettings { /** @return whether pref was changed */ private fun removeEnabledSubtype(prefs: SharedPreferences, subtype: SettingsSubtype): Boolean { - val oldSubtypes = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!! - .split(Separators.SETS).filter { it.isNotEmpty() }.map { it.toSettingsSubtype() } + val oldSubtypes = createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!) val newSubtypes = oldSubtypes - subtype if (oldSubtypes == newSubtypes) return false // already removed - prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newSubtypes.joinToString(Separators.SETS) { it.toPref() }) } + prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, createPrefSubtypes(newSubtypes)) } if (subtype == prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype()) { // switch subtype if the currently used one has been disabled try { diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt index 048a7410b..b9bce10a9 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt @@ -53,10 +53,11 @@ object SubtypeUtilsAdditional { val prefs = context.prefs() SubtypeSettings.removeEnabledSubtype(context, subtype) val oldAdditionalSubtypesString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!! - val oldAdditionalSubtypes = createAdditionalSubtypes(oldAdditionalSubtypesString) - val newAdditionalSubtypes = oldAdditionalSubtypes.filter { it != subtype } - val newAdditionalSubtypesString = createPrefSubtypes(newAdditionalSubtypes) - Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString) + val oldAdditionalSubtypes = SubtypeSettings.createSettingsSubtypes(oldAdditionalSubtypesString) + val settingsSubtype = subtype.toSettingsSubtype() + val newAdditionalSubtypes = oldAdditionalSubtypes.filter { it != settingsSubtype } + val newAdditionalSubtypesString = SubtypeSettings.createPrefSubtypes(newAdditionalSubtypes) + prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, newAdditionalSubtypesString).apply() } // updates additional subtypes, enabled subtypes, and selected subtype @@ -66,34 +67,37 @@ object SubtypeUtilsAdditional { val isSelected = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype() == from val isEnabled = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!.split(Separators.SETS) .any { it.toSettingsSubtype() == from } - val new = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!! - .split(Separators.SETS).mapNotNullTo(sortedSetOf()) { - if (it == from.toPref()) null else it - } + to.toPref() - prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, new.joinToString(Separators.SETS)).apply() - - val fromSubtype = from.toAdditionalSubtype() // will be null if we edit a resource subtype - val toSubtype = to.toAdditionalSubtype() // should never be null - if (isSelected && toSubtype != null) { - SubtypeSettings.setSelectedSubtype(prefs, toSubtype) + val additionalSubtypes = SubtypeSettings.createSettingsSubtypes(prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!) + .toMutableList() + additionalSubtypes.remove(from) + if (SubtypeSettings.getResourceSubtypesForLocale(to.locale).none { it.toSettingsSubtype() == to }) { + // We only add the "to" subtype if it's not equal to a resource subtype. + // This means we make additional subtype disappear as magically as it was added if all settings are default. + // If we don't do this, enabling the base subtype will result in the additional subtype being enabled, + // as both have the same settingsSubtype. + additionalSubtypes.add(to) } - if (fromSubtype != null && isEnabled && toSubtype != null) { - SubtypeSettings.removeEnabledSubtype(context, fromSubtype) - SubtypeSettings.addEnabledSubtype(prefs, toSubtype) + val editor = prefs.edit() + editor.putString(Settings.PREF_ADDITIONAL_SUBTYPES, SubtypeSettings.createPrefSubtypes(additionalSubtypes)) + if (isSelected) { + editor.putString(Settings.PREF_SELECTED_SUBTYPE, to.toPref()) } + if (isEnabled) { + val enabled = SubtypeSettings.createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!) + .toMutableList() + enabled.remove(from) + enabled.add(to) + editor.putString(Settings.PREF_ENABLED_SUBTYPES, SubtypeSettings.createPrefSubtypes(enabled)) + } + editor.apply() + SubtypeSettings.reloadEnabledSubtypes(context) } - fun createAdditionalSubtypes(prefSubtypes: String): List { - if (prefSubtypes.isEmpty()) - return emptyList() - return prefSubtypes.split(Separators.SETS).mapNotNull { it.toSettingsSubtype().toAdditionalSubtype() } - } - - fun createPrefSubtypes(subtypes: Collection): String { - if (subtypes.isEmpty()) - return "" - return subtypes.joinToString(Separators.SETS) { it.toSettingsSubtype().toPref() } - } + fun createAdditionalSubtypes(prefSubtypes: String): List = + prefSubtypes.split(Separators.SETS).mapNotNull { + if (it.isEmpty()) null + else it.toSettingsSubtype().toAdditionalSubtype() + } private fun getNameResId(locale: Locale, mainLayoutName: String): Int { val nameId = SubtypeLocaleUtils.getSubtypeNameResId(locale, mainLayoutName) diff --git a/app/src/main/java/helium314/keyboard/settings/dialogs/SubtypeDialog.kt b/app/src/main/java/helium314/keyboard/settings/dialogs/SubtypeDialog.kt index 7da713ec3..7db7cadbf 100644 --- a/app/src/main/java/helium314/keyboard/settings/dialogs/SubtypeDialog.kt +++ b/app/src/main/java/helium314/keyboard/settings/dialogs/SubtypeDialog.kt @@ -125,8 +125,8 @@ fun SubtypeDialog( onConfirmed = { onConfirmed(currentSubtype) }, neutralButtonText = if (initialSubtype.isAdditionalSubtype(prefs)) stringResource(R.string.delete) else null, onNeutral = { - SubtypeUtilsAdditional.removeAdditionalSubtype(ctx, initialSubtype.toAdditionalSubtype()!!) - SubtypeSettings.removeEnabledSubtype(ctx, initialSubtype.toAdditionalSubtype()!!) + SubtypeUtilsAdditional.removeAdditionalSubtype(ctx, initialSubtype.toAdditionalSubtype()) + SubtypeSettings.removeEnabledSubtype(ctx, initialSubtype.toAdditionalSubtype()) onDismissRequest() }, title = { @@ -393,7 +393,7 @@ private fun MainLayoutRow( if (showLayoutEditDialog != null) { val layoutName = showLayoutEditDialog!!.first val startContent = showLayoutEditDialog?.second - ?: if (layoutName in appLayouts) LayoutUtils.getContent(LayoutType.MAIN, layoutName, ctx) + ?: if (layoutName in appLayouts) LayoutUtils.getContentWithPlus(layoutName, currentSubtype.locale, ctx) else null LayoutEditDialog( onDismissRequest = { showLayoutEditDialog = null }, diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/LoadGestureLibPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/LoadGestureLibPreference.kt index 591d4a5c7..5b51bdbe2 100644 --- a/app/src/main/java/helium314/keyboard/settings/preferences/LoadGestureLibPreference.kt +++ b/app/src/main/java/helium314/keyboard/settings/preferences/LoadGestureLibPreference.kt @@ -35,10 +35,13 @@ fun LoadGestureLibPreference(setting: Setting) { val abi = Build.SUPPORTED_ABIS[0] val libFile = File(ctx.filesDir?.absolutePath + File.separator + JniUtils.JNI_LIB_IMPORT_FILE_NAME) fun renameToLibFileAndRestart(file: File, checksum: String) { + libFile.setWritable(true) libFile.delete() - // store checksum in default preferences (soo JniUtils) + // store checksum in default preferences (see JniUtils) prefs.edit().putString(Settings.PREF_LIBRARY_CHECKSUM, checksum).commit() - file.renameTo(libFile) + file.copyTo(libFile) + libFile.setReadOnly() + file.delete() Runtime.getRuntime().exit(0) // exit will restart the app, so library will be loaded } var tempFilePath: String? by rememberSaveable { mutableStateOf(null) } diff --git a/app/src/test/java/helium314/keyboard/SubtypeTest.kt b/app/src/test/java/helium314/keyboard/SubtypeTest.kt new file mode 100644 index 000000000..a101c07d9 --- /dev/null +++ b/app/src/test/java/helium314/keyboard/SubtypeTest.kt @@ -0,0 +1,75 @@ +package helium314.keyboard + +import helium314.keyboard.keyboard.KeyboardId +import helium314.keyboard.keyboard.KeyboardLayoutSet +import helium314.keyboard.keyboard.internal.KeyboardParams +import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL +import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams +import helium314.keyboard.latin.LatinIME +import helium314.keyboard.latin.common.LocaleUtils.constructLocale +import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.settings.SettingsSubtype.Companion.toSettingsSubtype +import helium314.keyboard.latin.utils.LayoutType +import helium314.keyboard.latin.utils.POPUP_KEYS_LAYOUT +import helium314.keyboard.latin.utils.SubtypeSettings +import helium314.keyboard.latin.utils.SubtypeUtilsAdditional +import helium314.keyboard.latin.utils.prefs +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowLog +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@RunWith(RobolectricTestRunner::class) +@Config(shadows = [ + ShadowInputMethodManager2::class +]) +class SubtypeTest { + private lateinit var latinIME: LatinIME + private lateinit var params: KeyboardParams + + @BeforeTest fun setUp() { + latinIME = Robolectric.setupService(LatinIME::class.java) + ShadowLog.setupLogging() + ShadowLog.stream = System.out + params = KeyboardParams() + params.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET) + params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT) + addLocaleKeyTextsToParams(latinIME, params, POPUP_KEYS_NORMAL) + } + + @Test fun emptyAdditionalSubtypesResultsInEmptyList() { + // avoid issues where empty string results in additional subtype for undefined locale + val prefs = latinIME.prefs() + prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, "").apply() + assertTrue(SubtypeSettings.getAdditionalSubtypes().isEmpty()) + val from = SubtypeSettings.getResourceSubtypesForLocale("es".constructLocale()).first() + + // no change, and "changed" subtype actually is resource subtype -> still expect empty list + SubtypeUtilsAdditional.changeAdditionalSubtype(from.toSettingsSubtype(), from.toSettingsSubtype(), latinIME) + assertEquals(emptyList(), SubtypeSettings.getAdditionalSubtypes().map { it.toSettingsSubtype() }) + } + + @Test fun subtypeStaysEnabledOnEdits() { + val prefs = latinIME.prefs() + prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, "").apply() // clear it for convenience + + // edit enabled resource subtype + val from = SubtypeSettings.getResourceSubtypesForLocale("es".constructLocale()).first() + SubtypeSettings.addEnabledSubtype(prefs, from) + val to = from.toSettingsSubtype().withLayout(LayoutType.SYMBOLS, "symbols_arabic") + SubtypeUtilsAdditional.changeAdditionalSubtype(from.toSettingsSubtype(), to, latinIME) + assertEquals(to, SubtypeSettings.getEnabledSubtypes(false).single().toSettingsSubtype()) + + // change the new subtype to effectively be the same as original resource subtype + val toNew = to.withoutLayout(LayoutType.SYMBOLS) + assertEquals(from.toSettingsSubtype(), toNew) + SubtypeUtilsAdditional.changeAdditionalSubtype(to, toNew, latinIME) + assertEquals(emptyList(), SubtypeSettings.getAdditionalSubtypes().map { it.toSettingsSubtype() }) + assertEquals(from.toSettingsSubtype(), SubtypeSettings.getEnabledSubtypes(false).single().toSettingsSubtype()) + } +}