diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e3e590e32..53770ed2e 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "helium314.keyboard" minSdk = 21 targetSdk = 34 - versionCode = 2306 - versionName = "2.3+dev5" + versionCode = 2307 + versionName = "2.3+dev6" ndk { abiFilters.clear() abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")) diff --git a/app/src/main/java/helium314/keyboard/latin/App.kt b/app/src/main/java/helium314/keyboard/latin/App.kt index fb1c07d3e..5e60a37ee 100644 --- a/app/src/main/java/helium314/keyboard/latin/App.kt +++ b/app/src/main/java/helium314/keyboard/latin/App.kt @@ -9,6 +9,7 @@ import helium314.keyboard.keyboard.KeyboardTheme import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode import helium314.keyboard.latin.common.ColorType import helium314.keyboard.latin.common.Constants.Separators +import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue import helium314.keyboard.latin.common.LocaleUtils.constructLocale import helium314.keyboard.latin.common.encodeBase36 import helium314.keyboard.latin.settings.Defaults @@ -22,8 +23,14 @@ import helium314.keyboard.latin.utils.LayoutType.Companion.folder import helium314.keyboard.latin.utils.LayoutUtilsCustom import helium314.keyboard.latin.utils.ScriptUtils.SCRIPT_LATIN import helium314.keyboard.latin.utils.ScriptUtils.script +import helium314.keyboard.latin.utils.SettingsSubtype +import helium314.keyboard.latin.utils.SettingsSubtype.Companion.toSettingsSubtype +import helium314.keyboard.latin.utils.SubtypeUtilsAdditional import helium314.keyboard.latin.utils.ToolbarKey import helium314.keyboard.latin.utils.defaultPinnedToolbarPref +import helium314.keyboard.latin.utils.getResourceSubtypes +import helium314.keyboard.latin.utils.locale +import helium314.keyboard.latin.utils.mainLayoutName import helium314.keyboard.latin.utils.prefs import helium314.keyboard.latin.utils.protectedPrefs import helium314.keyboard.latin.utils.upgradeToolbarPrefs @@ -361,7 +368,7 @@ fun checkVersionUpgrade(context: Context) { if (locale.script() != SCRIPT_LATIN) return@forEach // change language tag to SCRIPT_LATIN, but // avoid overwriting if 2 layouts have a different language tag, but the same name - val layoutDisplayName = LayoutUtilsCustom.getSecondaryLayoutDisplayName(it.name) + val layoutDisplayName = LayoutUtilsCustom.getDisplayName(it.name) var newFile = File(it.parentFile!!, LayoutUtilsCustom.getMainLayoutName(layoutDisplayName, locale)) var i = 1 while (newFile.exists()) // make sure name is not already in use, e.g. custom.en.abcd. and custom.it.abcd. would both be custom.Latn.abcd @@ -392,6 +399,54 @@ fun checkVersionUpgrade(context: Context) { prefs.edit().putString(it, newValue).apply() } } + if (oldVersion <= 2306) { + // upgrade additional, enabled, and selected subtypes to same format of locale and (filtered) extra value + if (prefs.contains(Settings.PREF_ADDITIONAL_SUBTYPES)) { + val new = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, "")!!.split(Separators.SETS).mapNotNull { pref -> + val oldSplit = pref.split(Separators.SET) + val languageTag = oldSplit[0] + val mainLayoutName = oldSplit[1] + val extraValue = oldSplit[2] + SettingsSubtype( + languageTag.constructLocale(), + ExtraValue.KEYBOARD_LAYOUT_SET + "=MAIN" + Separators.KV + mainLayoutName + "," + extraValue + ).toAdditionalSubtype()?.let { it.toSettingsSubtype().toPref() } + }.joinToString(Separators.SETS) + prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, new).apply() + } + listOf(Settings.PREF_ENABLED_SUBTYPES, Settings.PREF_SELECTED_SUBTYPE).forEach { key -> + if (!prefs.contains(key)) return@forEach + val resourceSubtypes = getResourceSubtypes(context.resources) + val additionalSubtypeString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!! + val additionalSubtypes = SubtypeUtilsAdditional.createAdditionalSubtypes(additionalSubtypeString) + val new = prefs.getString(key, "")!!.split(Separators.SETS).joinToString(Separators.SETS) { pref -> + val oldSplit = pref.split(Separators.SET) + val languageTag = oldSplit[0] + val mainLayoutName = oldSplit[1] + // we now need more information than just locale and main layout name, get it from existing subtypes + val filtered = additionalSubtypes.filter { + it.locale().toLanguageTag() == languageTag && (it.mainLayoutName() ?: "qwerty") == mainLayoutName + } + if (filtered.isNotEmpty()) + return@joinToString filtered.first().toSettingsSubtype().toPref() + // find best matching resource subtype + val goodMatch = resourceSubtypes.filter { + it.locale().toLanguageTag() == languageTag && (it.mainLayoutName() ?: "qwerty") == mainLayoutName + } + if (goodMatch.isNotEmpty()) + return@joinToString goodMatch.first().toSettingsSubtype().toPref() + // not sure how we can get here, but better deal with it + val okMatch = resourceSubtypes.filter { + it.locale().language == languageTag.constructLocale().language && (it.mainLayoutName() ?: "qwerty") == mainLayoutName + } + if (okMatch.isNotEmpty()) + okMatch.first().toSettingsSubtype().toPref() + else resourceSubtypes.first { it.locale().language == languageTag.constructLocale().language } + .toSettingsSubtype().toPref() + } + prefs.edit().putString(key, new).apply() + } + } upgradeToolbarPrefs(prefs) LayoutUtilsCustom.onLayoutFileChanged() // just to be sure prefs.edit { putInt(Settings.PREF_VERSION_CODE, BuildConfig.VERSION_CODE) } diff --git a/app/src/main/java/helium314/keyboard/latin/InputAttributes.java b/app/src/main/java/helium314/keyboard/latin/InputAttributes.java index 92c75991b..a8b51659a 100644 --- a/app/src/main/java/helium314/keyboard/latin/InputAttributes.java +++ b/app/src/main/java/helium314/keyboard/latin/InputAttributes.java @@ -8,10 +8,10 @@ package helium314.keyboard.latin; import android.os.Build; import android.text.InputType; -import helium314.keyboard.latin.utils.Log; import android.view.inputmethod.EditorInfo; -import helium314.keyboard.latin.common.StringUtils; +import helium314.keyboard.latin.common.StringUtilsKt; +import helium314.keyboard.latin.utils.Log; import helium314.keyboard.latin.utils.InputTypeUtils; import java.util.ArrayList; @@ -256,10 +256,9 @@ public final class InputAttributes { mTargetApplicationPackageName); } - public static boolean inPrivateImeOptions(final String packageName, final String key, - final EditorInfo editorInfo) { + public static boolean inPrivateImeOptions(final String packageName, final String key, final EditorInfo editorInfo) { if (editorInfo == null) return false; final String findingKey = (packageName != null) ? packageName + "." + key : key; - return StringUtils.containsInCommaSplittableText(findingKey, editorInfo.privateImeOptions); + return StringUtilsKt.containsValueWhenSplit(editorInfo.privateImeOptions, findingKey, ","); } } diff --git a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.java b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.java index 6aa5f83bc..e191c359f 100644 --- a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.java +++ b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.java @@ -123,51 +123,6 @@ public final class StringUtils { return new String(Character.toChars(codePoint)); } - public static boolean containsInArray(@NonNull final String text, - @NonNull final String[] array) { - for (final String element : array) { - if (text.equals(element)) { - return true; - } - } - return false; - } - - /** - * Comma-Splittable Text is similar to Comma-Separated Values (CSV) but has much simpler syntax. - * Unlike CSV, Comma-Splittable Text has no escaping mechanism, so that the text can't contain - * a comma character in it. - */ - @NonNull - private static final String SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT = ","; - - public static boolean containsInCommaSplittableText(@NonNull final String text, - @Nullable final String extraValues) { - if (isEmpty(extraValues)) { - return false; - } - return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT)); - } - - @NonNull - public static String removeFromCommaSplittableTextIfExists(@NonNull final String text, - @Nullable final String extraValues) { - if (isEmpty(extraValues)) { - return EMPTY_STRING; - } - final String[] elements = extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT); - if (!containsInArray(text, elements)) { - return extraValues; - } - final ArrayList result = new ArrayList<>(elements.length - 1); - for (final String element : elements) { - if (!text.equals(element)) { - result.add(element); - } - } - return join(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT, result); - } - /** * Remove duplicates from an array of strings. *

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 8a9c9ddf4..5b8af87c3 100644 --- a/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/common/StringUtils.kt @@ -129,6 +129,11 @@ fun encodeBase36(string: String): String = BigInteger(string.toByteArray()).toSt fun decodeBase36(string: String) = BigInteger(string, 36).toByteArray().decodeToString() +fun containsValueWhenSplit(string: String?, value: String, split: String): Boolean { + if (string == null) return false + return string.split(split).contains(value) +} + fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c)) fun isEmoji(s: CharSequence): Boolean = mightBeEmoji(s) && s.matches(emoRegex) diff --git a/app/src/main/java/helium314/keyboard/latin/settings/LanguageSettingsDialog.kt b/app/src/main/java/helium314/keyboard/latin/settings/LanguageSettingsDialog.kt index 3daa37d13..f805bc081 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/LanguageSettingsDialog.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/LanguageSettingsDialog.kt @@ -209,7 +209,7 @@ class LanguageSettingsDialog( reloadSetting() } if (isCustom) { - confirmDialog(context, context.getString(R.string.delete_layout, LayoutUtilsCustom.getSecondaryLayoutDisplayName(layoutSetName)), context.getString(R.string.delete)) { delete() } + confirmDialog(context, context.getString(R.string.delete_layout, LayoutUtilsCustom.getDisplayName(layoutSetName)), context.getString(R.string.delete)) { delete() } } else { delete() } 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 28d17f695..c553a683e 100644 --- a/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java +++ b/app/src/main/java/helium314/keyboard/latin/spellcheck/AndroidSpellCheckerService.java @@ -199,8 +199,8 @@ public final class AndroidSpellCheckerService extends SpellCheckerService editorInfo.inputType = InputType.TYPE_CLASS_TEXT; Settings.getInstance().loadSettings(this, locale, new InputAttributes(editorInfo, false, getPackageName())); } - final String keyboardLayoutName = SubtypeSettingsKt.getMatchingLayoutSetNameForLocale(locale); - final InputMethodSubtype subtype = SubtypeUtilsAdditional.INSTANCE.createDummyAdditionalSubtype(locale, keyboardLayoutName); + final String mainLayoutName = SubtypeSettingsKt.getMatchingMainLayoutNameForLocale(locale); + final InputMethodSubtype subtype = SubtypeUtilsAdditional.INSTANCE.createDummyAdditionalSubtype(locale, mainLayoutName); final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype); return keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); } 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 3759c00f2..d1d422772 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutType.kt @@ -2,6 +2,7 @@ package helium314.keyboard.latin.utils import helium314.keyboard.latin.R import helium314.keyboard.latin.common.Constants.Separators +import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue import java.io.File import java.util.EnumMap @@ -12,9 +13,9 @@ enum class LayoutType { companion object { fun EnumMap.toExtraValue() = map { it.key.name + Separators.KV + it.value }.joinToString(Separators.ENTRY) - fun getLayoutMap(extraValue: String): EnumMap { + fun getLayoutMap(string: String): EnumMap { val map = EnumMap(LayoutType::class.java) - extraValue.split(Separators.ENTRY).forEach { + string.split(Separators.ENTRY).forEach { val s = it.split(Separators.KV) runCatching { map[LayoutType.valueOf(s[0])] = s[1] } } @@ -37,5 +38,12 @@ enum class LayoutType { EMOJI_BOTTOM -> R.string.layout_emoji_bottom_row CLIPBOARD_BOTTOM -> R.string.layout_clip_bottom_row } + + fun getMainLayoutFromExtraValue(extraValue: String): String? { + val value = extraValue.split(",") + .firstOrNull { it.startsWith("${ExtraValue.KEYBOARD_LAYOUT_SET}=") }?.substringAfter("=") + if (value == null) return null + return getLayoutMap(value)[MAIN] + } } } 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 5888d568d..b2613666b 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtilsCustom.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/LayoutUtilsCustom.kt @@ -166,7 +166,7 @@ object LayoutUtilsCustom { customLayoutMap.clear() } - fun getSecondaryLayoutDisplayName(layoutName: String) = + fun getDisplayName(layoutName: String) = try { if (layoutName.count { it == '.' } == 3) // main layout: "custom...", other: custom.. decodeBase36(layoutName.substringAfter(CUSTOM_LAYOUT_PREFIX).substringAfter(".").substringBeforeLast(".")) @@ -175,7 +175,7 @@ object LayoutUtilsCustom { layoutName } - fun getLayoutName(displayName: String) = CUSTOM_LAYOUT_PREFIX + encodeBase36(displayName) + "." + fun getSecondaryLayoutName(displayName: String) = CUSTOM_LAYOUT_PREFIX + encodeBase36(displayName) + "." fun getMainLayoutName(displayName: String, locale: Locale) = if (locale.script() == ScriptUtils.SCRIPT_LATIN) @@ -196,7 +196,7 @@ object LayoutUtilsCustom { setText(startContent ?: file.readText()) } val builder = AlertDialog.Builder(context) - .setTitle(getSecondaryLayoutDisplayName(layoutName)) + .setTitle(getDisplayName(layoutName)) .setView(editText) .setPositiveButton(R.string.save) { _, _ -> val content = editText.text.toString() diff --git a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.java b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.java index 18de829fe..5d7513517 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.java +++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeLocaleUtils.java @@ -267,7 +267,7 @@ public final class SubtypeLocaleUtils { @Nullable public static String getMainLayoutDisplayName(@NonNull final String layoutName) { if (LayoutUtilsCustom.INSTANCE.isCustomLayout(layoutName)) - return LayoutUtilsCustom.INSTANCE.getSecondaryLayoutDisplayName(layoutName); + return LayoutUtilsCustom.INSTANCE.getDisplayName(layoutName); return sKeyboardLayoutToDisplayNameMap.get(layoutName); } 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 dc23a3367..9594da4c5 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeSettings.kt @@ -5,22 +5,19 @@ package helium314.keyboard.latin.utils import android.content.Context import android.content.SharedPreferences import android.content.res.Resources -import android.os.Build import android.view.inputmethod.InputMethodSubtype import android.widget.Toast import androidx.core.app.LocaleManagerCompat import androidx.core.content.edit import helium314.keyboard.keyboard.KeyboardSwitcher -import helium314.keyboard.latin.R import helium314.keyboard.latin.RichInputMethodManager import helium314.keyboard.latin.common.Constants.Separators import helium314.keyboard.latin.common.LocaleUtils -import helium314.keyboard.latin.common.LocaleUtils.constructLocale import helium314.keyboard.latin.define.DebugFlags import helium314.keyboard.latin.settings.Defaults import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.utils.ScriptUtils.script -import org.xmlpull.v1.XmlPullParser +import helium314.keyboard.latin.utils.SettingsSubtype.Companion.toSettingsSubtype import java.util.Locale /** @return enabled subtypes. If no subtypes are enabled, but a contextForFallback is provided, @@ -39,7 +36,7 @@ fun getAllAvailableSubtypes(): List { return resourceSubtypesByLocale.values.flatten() + additionalSubtypes } -fun getMatchingLayoutSetNameForLocale(locale: Locale): String { +fun getMatchingMainLayoutNameForLocale(locale: Locale): String { val subtypes = resourceSubtypesByLocale.values.flatten() val name = LocaleUtils.getBestMatch(locale, subtypes) { it.locale() }?.mainLayoutName() if (name != null) return name @@ -57,7 +54,7 @@ fun getMatchingLayoutSetNameForLocale(locale: Locale): String { fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) { require(initialized) - val subtypeString = newSubtype.prefString() + val subtypeString = newSubtype.toSettingsSubtype().toPref() val oldSubtypeStrings = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!.split(Separators.SETS) val newString = (oldSubtypeStrings + subtypeString).filter { it.isNotBlank() }.toSortedSet().joinToString(Separators.SETS) prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newString) } @@ -72,32 +69,37 @@ fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) /** returns whether subtype was actually removed, does not remove last subtype */ fun removeEnabledSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) { require(initialized) - removeEnabledSubtype(prefs, subtype.prefString()) + removeEnabledSubtype(prefs, subtype.toSettingsSubtype().toPref()) enabledSubtypes.remove(subtype) RichInputMethodManager.getInstance().refreshSubtypeCaches() } fun getSelectedSubtype(prefs: SharedPreferences): InputMethodSubtype { require(initialized) - val localeAndLayout = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toLocaleAndLayout() + val selectedSubtype = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype() + val selectedAdditionalSubtype = selectedSubtype.toAdditionalSubtype() + if (selectedAdditionalSubtype != null && additionalSubtypes.contains(selectedAdditionalSubtype)) + return selectedAdditionalSubtype // don't even care whether it's enabled + // no additional subtype, must be a resource subtype val subtypes = if (prefs.getBoolean(Settings.PREF_USE_SYSTEM_LOCALES, Defaults.PREF_USE_SYSTEM_LOCALES)) getDefaultEnabledSubtypes() else enabledSubtypes - val subtype = subtypes.firstOrNull { localeAndLayout.first == it.locale() && localeAndLayout.second == SubtypeLocaleUtils.getMainLayoutName(it) } + + val subtype = subtypes.firstOrNull { it.toSettingsSubtype() == selectedSubtype } if (subtype != null) { return subtype } else { - Log.w(TAG, "selected subtype $localeAndLayout / ${prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)} not found") + Log.w(TAG, "selected subtype $selectedSubtype / ${prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)} not found") } if (subtypes.isNotEmpty()) return subtypes.first() val defaultSubtypes = getDefaultEnabledSubtypes() - return defaultSubtypes.firstOrNull { localeAndLayout.first == it.locale() && localeAndLayout.second == SubtypeLocaleUtils.getMainLayoutName(it) } - ?: defaultSubtypes.firstOrNull { localeAndLayout.first.language == it.locale().language && localeAndLayout.second == SubtypeLocaleUtils.getMainLayoutName(it) } + return defaultSubtypes.firstOrNull { it.locale() == selectedSubtype.locale && it.mainLayoutName() == it.mainLayoutName() } + ?: defaultSubtypes.firstOrNull { it.locale().language == selectedSubtype.locale.language } ?: defaultSubtypes.first() } fun setSelectedSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) { - val subtypeString = subtype.prefString() + val subtypeString = subtype.toSettingsSubtype().toPref() if (subtype.locale().toLanguageTag().isEmpty() || prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE) == subtypeString) return prefs.edit { putString(Settings.PREF_SELECTED_SUBTYPE, subtypeString) } @@ -180,52 +182,9 @@ private fun getDefaultEnabledSubtypes(): List { return systemSubtypes } -/** string for for identifying a subtype, does not contain all necessary information to actually create it */ -private fun InputMethodSubtype.prefString(): String { - if (DebugFlags.DEBUG_ENABLED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && locale().toLanguageTag() == "und") { - @Suppress("deprecation") // it's debug logging, better get all information - Log.e(TAG, "unknown language, should not happen ${locale}, $languageTag, $extraValue, ${hashCode()}, $nameResId") - } - return locale().toLanguageTag() + Separators.SET + SubtypeLocaleUtils.getMainLayoutName(this) -} - -private fun String.toLocaleAndLayout(): Pair = - substringBefore(Separators.SET).constructLocale() to substringAfter(Separators.SET) - -private fun Pair.prefString() = - first.toLanguageTag() + Separators.SET + second - private fun loadResourceSubtypes(resources: Resources) { - val xml = resources.getXml(R.xml.method) - xml.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true) - val namespace = "http://schemas.android.com/apk/res/android" - var eventType = xml.eventType - while (eventType != XmlPullParser.END_DOCUMENT) { - if (eventType == XmlPullParser.START_TAG && xml.name == "subtype") { - val icon = xml.getAttributeResourceValue(namespace, "icon", 0) - val label = xml.getAttributeResourceValue(namespace, "label", 0) - val subtypeId = xml.getAttributeIntValue(namespace, "subtypeId", 0) - val localeString = xml.getAttributeValue(namespace, "imeSubtypeLocale").intern() - val languageTag = xml.getAttributeValue(namespace, "languageTag").intern() - val imeSubtypeMode = xml.getAttributeValue(namespace, "imeSubtypeMode") - val imeSubtypeExtraValue = xml.getAttributeValue(namespace, "imeSubtypeExtraValue").intern() - val isAsciiCapable = xml.getAttributeBooleanValue(namespace, "isAsciiCapable", false) - val b = InputMethodSubtype.InputMethodSubtypeBuilder() - b.setSubtypeIconResId(icon) - b.setSubtypeNameResId(label) - if (subtypeId != 0) - b.setSubtypeId(subtypeId) - b.setSubtypeLocale(localeString) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - b.setLanguageTag(languageTag) - b.setSubtypeMode(imeSubtypeMode) - b.setSubtypeExtraValue(imeSubtypeExtraValue) - b.setIsAsciiCapable(isAsciiCapable) - val locale = if (languageTag.isEmpty()) localeString.constructLocale() - else languageTag.constructLocale() - resourceSubtypesByLocale.getOrPut(locale) { ArrayList(2) }.add(b.build()) - } - eventType = xml.next() + getResourceSubtypes(resources).forEach { + resourceSubtypesByLocale.getOrPut(it.locale()) { ArrayList(2) }.add(it) } } @@ -255,30 +214,33 @@ private fun loadAdditionalSubtypes(prefs: SharedPreferences) { // requires loadResourceSubtypes to be called before private fun loadEnabledSubtypes(context: Context) { val prefs = context.prefs() - val subtypeStrings = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!! - .split(Separators.SETS).filter { it.isNotEmpty() }.map { it.toLocaleAndLayout() } - - for (localeAndLayout in subtypeStrings) { - val subtypesForLocale = resourceSubtypesByLocale[localeAndLayout.first] + val settingsSubtypes = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!! + .split(Separators.SETS).filter { it.isNotEmpty() }.map { it.toSettingsSubtype() } + for (settingsSubtype in settingsSubtypes) { + val additionalSubtype = settingsSubtype.toAdditionalSubtype() + if (additionalSubtype != null && additionalSubtypes.contains(additionalSubtype)) { + enabledSubtypes.add(additionalSubtype) + continue + } + val subtypesForLocale = resourceSubtypesByLocale[settingsSubtype.locale] if (subtypesForLocale == null) { - val message = "no resource subtype for $localeAndLayout" + val message = "no resource subtype for $settingsSubtype" Log.w(TAG, message) if (DebugFlags.DEBUG_ENABLED) Toast.makeText(context, message, Toast.LENGTH_LONG).show() else // don't remove in debug mode - removeEnabledSubtype(prefs, localeAndLayout.prefString()) + removeEnabledSubtype(prefs, settingsSubtype.toPref()) continue } - val subtype = subtypesForLocale.firstOrNull { SubtypeLocaleUtils.getMainLayoutName(it) == localeAndLayout.second } - ?: additionalSubtypes.firstOrNull { it.locale() == localeAndLayout.first && SubtypeLocaleUtils.getMainLayoutName(it) == localeAndLayout.second } + val subtype = subtypesForLocale.firstOrNull { SubtypeLocaleUtils.getMainLayoutName(it) == (settingsSubtype.mainLayoutName() ?: "qwerty") } if (subtype == null) { - val message = "subtype $localeAndLayout could not be loaded" + val message = "subtype $settingsSubtype could not be loaded" Log.w(TAG, message) if (DebugFlags.DEBUG_ENABLED) Toast.makeText(context, message, Toast.LENGTH_LONG).show() else // don't remove in debug mode - removeEnabledSubtype(prefs, localeAndLayout.prefString()) + removeEnabledSubtype(prefs, settingsSubtype.toPref()) continue } @@ -296,7 +258,7 @@ private fun removeEnabledSubtype(prefs: SharedPreferences, subtypeString: String // switch subtype if the currently used one has been disabled try { val nextSubtype = RichInputMethodManager.getInstance().getNextSubtypeInThisIme(true) - if (subtypeString == nextSubtype?.prefString()) + if (subtypeString == nextSubtype?.toSettingsSubtype()?.toPref()) KeyboardSwitcher.getInstance().switchToSubtype(getDefaultEnabledSubtypes().first()) else KeyboardSwitcher.getInstance().switchToSubtype(nextSubtype) 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 f1515313d..dc0dd96a2 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtils.kt @@ -1,11 +1,18 @@ package helium314.keyboard.latin.utils import android.content.Context +import android.content.res.Resources import android.os.Build import android.view.inputmethod.InputMethodSubtype +import helium314.keyboard.latin.R +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 import helium314.keyboard.latin.common.LocaleUtils.constructLocale +import helium314.keyboard.latin.define.DebugFlags +import helium314.keyboard.latin.utils.ScriptUtils.script +import org.xmlpull.v1.XmlPullParser import java.util.Locale fun InputMethodSubtype.locale(): Locale { @@ -21,11 +28,92 @@ fun InputMethodSubtype.mainLayoutName(): String? { return map[LayoutType.MAIN] } +fun getResourceSubtypes(resources: Resources): List { + val subtypes = mutableListOf() + val xml = resources.getXml(R.xml.method) + xml.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true) + val namespace = "http://schemas.android.com/apk/res/android" + var eventType = xml.eventType + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG && xml.name == "subtype") { + val icon = xml.getAttributeResourceValue(namespace, "icon", 0) + val label = xml.getAttributeResourceValue(namespace, "label", 0) + val subtypeId = xml.getAttributeIntValue(namespace, "subtypeId", 0) + val localeString = xml.getAttributeValue(namespace, "imeSubtypeLocale").intern() + val languageTag = xml.getAttributeValue(namespace, "languageTag").intern() + val imeSubtypeMode = xml.getAttributeValue(namespace, "imeSubtypeMode") + val imeSubtypeExtraValue = xml.getAttributeValue(namespace, "imeSubtypeExtraValue").intern() + val isAsciiCapable = xml.getAttributeBooleanValue(namespace, "isAsciiCapable", false) + val b = InputMethodSubtype.InputMethodSubtypeBuilder() + b.setSubtypeIconResId(icon) + b.setSubtypeNameResId(label) + if (subtypeId != 0) + b.setSubtypeId(subtypeId) + b.setSubtypeLocale(localeString) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + b.setLanguageTag(languageTag) + b.setSubtypeMode(imeSubtypeMode) + b.setSubtypeExtraValue(imeSubtypeExtraValue) + b.setIsAsciiCapable(isAsciiCapable) + subtypes.add(b.build()) + } + eventType = xml.next() + } + 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): CharSequence { val layoutName = SubtypeLocaleUtils.getMainLayoutName(this) if (LayoutUtilsCustom.isCustomLayout(layoutName)) - return "${LocaleUtils.getLocaleDisplayNameInSystemLocale(locale(), context)} (${LayoutUtilsCustom.getSecondaryLayoutDisplayName(layoutName)})" + return "${LocaleUtils.getLocaleDisplayNameInSystemLocale(locale(), context)} (${LayoutUtilsCustom.getDisplayName(layoutName)})" return SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(this) } + +data class SettingsSubtype(val locale: Locale, val extraValue: String) { + + fun toPref() = locale.toLanguageTag() + Separators.SET + extraValue + + /** Creates an additional subtype from the SettingsSubtype. + * Resulting InputMethodSubtypes are equal if SettingsSubtypes are equal */ + fun toAdditionalSubtype(): InputMethodSubtype? { + val asciiCapable = locale.script() == ScriptUtils.SCRIPT_LATIN + val subtype = SubtypeUtilsAdditional.createAdditionalSubtype(locale, extraValue, asciiCapable, true) + if (subtype.nameResId == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT && !LayoutUtilsCustom.isCustomLayout(mainLayoutName() ?: "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 + } + + fun mainLayoutName() = LayoutType.getMainLayoutFromExtraValue(extraValue) + + companion object { + fun String.toSettingsSubtype() = + SettingsSubtype(substringBefore(Separators.SET).constructLocale(), substringAfter(Separators.SET)) + + /** Creates a SettingsSubtype from the given InputMethodSubtype. + * Will strip some extra values that are set when creating the InputMethodSubtype from SettingsSubtype */ + fun InputMethodSubtype.toSettingsSubtype(): SettingsSubtype { + if (DebugFlags.DEBUG_ENABLED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && locale().toLanguageTag() == "und") { + @Suppress("deprecation") // it's debug logging, better get all information + Log.e(SettingsSubtype::class.simpleName, "unknown language, should not happen ${locale}, $languageTag, $extraValue, ${hashCode()}, $nameResId") + } + val filteredExtraValue = extraValue.split(",").filterNot { + it == ExtraValue.ASCII_CAPABLE + || it == ExtraValue.EMOJI_CAPABLE + || it == ExtraValue.IS_ADDITIONAL_SUBTYPE + // todo: this is in "old" additional subtypes, but where was it set? + // must have been by app in 2.3, but not any more? + // anyway, a. we can easily create it again, and b. it may contain "bad" characters messing up the extra value + || it.startsWith(ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME) + }.joinToString(",") + require(!filteredExtraValue.contains(Separators.SETS) && !filteredExtraValue.contains(Separators.SET)) + { "extra value contains not allowed characters $filteredExtraValue" } + return SettingsSubtype(locale(), filteredExtraValue) + } + } +} 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 ca98ef0f4..457ab2e29 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/SubtypeUtilsAdditional.kt @@ -2,60 +2,60 @@ package helium314.keyboard.latin.utils import android.content.SharedPreferences import android.os.Build -import android.text.TextUtils import android.view.inputmethod.InputMethodSubtype import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder import helium314.keyboard.latin.R 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.common.LocaleUtils.constructLocale -import helium314.keyboard.latin.common.StringUtils import helium314.keyboard.latin.settings.Defaults import helium314.keyboard.latin.settings.Settings -import helium314.keyboard.latin.utils.LayoutUtilsCustom.isCustomLayout -import helium314.keyboard.latin.utils.ScriptUtils.script +import helium314.keyboard.latin.utils.SettingsSubtype.Companion.toSettingsSubtype import java.util.Locale object SubtypeUtilsAdditional { + fun isAdditionalSubtype(subtype: InputMethodSubtype): Boolean { return subtype.containsExtraValueKey(ExtraValue.IS_ADDITIONAL_SUBTYPE) } - private fun createAdditionalSubtypeInternal(locale: Locale, keyboardLayoutSetName: String, - isAsciiCapable: Boolean, isEmojiCapable: Boolean - ): InputMethodSubtype { - val nameId = SubtypeLocaleUtils.getSubtypeNameResId(locale, keyboardLayoutSetName) - val platformVersionDependentExtraValues = - getPlatformVersionDependentExtraValue(locale, keyboardLayoutSetName, isAsciiCapable, isEmojiCapable) - val platformVersionIndependentSubtypeId = - getPlatformVersionIndependentSubtypeId(locale, keyboardLayoutSetName) + // todo: extra value does not contain UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME for custom layout + // it did contain that key in 2.3, but where was it set? anyway, need to be careful with separators if we want to use it + // see also todo in SettingsSubtype + fun createAdditionalSubtype(locale: Locale, extraValue: String, isAsciiCapable: Boolean, + isEmojiCapable: Boolean): InputMethodSubtype { + val mainLayoutName = LayoutType.getMainLayoutFromExtraValue(extraValue) ?: "qwerty" + val nameId = SubtypeLocaleUtils.getSubtypeNameResId(locale, mainLayoutName) + val fullExtraValue = extraValue + "," + getAdditionalExtraValues(locale, mainLayoutName, isAsciiCapable, isEmojiCapable) + val subtypeId = getSubtypeId(locale, fullExtraValue, isAsciiCapable) val builder = InputMethodSubtypeBuilder() .setSubtypeNameResId(nameId) .setSubtypeIconResId(R.drawable.ic_ime_switcher) .setSubtypeLocale(locale.toString()) .setSubtypeMode(Constants.Subtype.KEYBOARD_MODE) - .setSubtypeExtraValue(platformVersionDependentExtraValues) + .setSubtypeExtraValue(fullExtraValue) .setIsAuxiliary(false) .setOverridesImplicitlyEnabledSubtype(false) - .setSubtypeId(platformVersionIndependentSubtypeId) + .setSubtypeId(subtypeId) .setIsAsciiCapable(isAsciiCapable) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) builder.setLanguageTag(locale.toLanguageTag()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && LayoutUtilsCustom.isCustomLayout(mainLayoutName)) + builder.setSubtypeNameOverride(LayoutUtilsCustom.getDisplayName(mainLayoutName)) return builder.build() } - fun createDummyAdditionalSubtype(locale: Locale, keyboardLayoutSetName: String) = - createAdditionalSubtypeInternal(locale, keyboardLayoutSetName, false, false) + fun createDummyAdditionalSubtype(locale: Locale, mainLayoutName: String) = + createAdditionalSubtype(locale, "MAIN${Separators.KV}$mainLayoutName", false, false) - fun createEmojiCapableAdditionalSubtype(locale: Locale, keyboardLayoutSetName: String, asciiCapable: Boolean) = - createAdditionalSubtypeInternal(locale, keyboardLayoutSetName, asciiCapable, true) + fun createEmojiCapableAdditionalSubtype(locale: Locale, mainLayoutName: String, asciiCapable: Boolean) = + createAdditionalSubtype(locale, "MAIN${Separators.KV}$mainLayoutName", asciiCapable, true) fun addAdditionalSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) { val oldAdditionalSubtypesString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!! val additionalSubtypes = createAdditionalSubtypes(oldAdditionalSubtypesString).toMutableSet() additionalSubtypes.add(subtype) - val newAdditionalSubtypesString = createPrefSubtypes(additionalSubtypes.toTypedArray()) + val newAdditionalSubtypesString = createPrefSubtypes(additionalSubtypes) Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString) } @@ -63,123 +63,25 @@ object SubtypeUtilsAdditional { 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.toTypedArray()) + val newAdditionalSubtypesString = createPrefSubtypes(newAdditionalSubtypes) Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString) } - // todo: adjust so we can store more stuff in extra values - private fun getPrefSubtype(subtype: InputMethodSubtype): String { - val mainLayoutName = SubtypeLocaleUtils.getMainLayoutName(subtype) - val layoutExtraValue = ExtraValue.KEYBOARD_LAYOUT_SET + "=MAIN" + Separators.KV + mainLayoutName - val extraValue = StringUtils.removeFromCommaSplittableTextIfExists( - layoutExtraValue, StringUtils.removeFromCommaSplittableTextIfExists( - ExtraValue.IS_ADDITIONAL_SUBTYPE, subtype.extraValue - ) - ) - require(!extraValue.contains(Separators.SETS) && !extraValue.contains(Separators.SET)) - { "extra value contains not allowed characters $extraValue" } - val basePrefSubtype = subtype.locale().toLanguageTag() + Separators.SET + mainLayoutName - return if (extraValue.isEmpty()) basePrefSubtype - else basePrefSubtype + Separators.SET + extraValue - } - fun createAdditionalSubtypes(prefSubtypes: String): List { - if (TextUtils.isEmpty(prefSubtypes)) { + if (prefSubtypes.isEmpty()) return emptyList() - } - return prefSubtypes.split(Separators.SETS) - .mapNotNull { createSubtypeFromString(it) } + return prefSubtypes.split(Separators.SETS).mapNotNull { it.toSettingsSubtype().toAdditionalSubtype() } } - // use string created with getPrefSubtype - fun createSubtypeFromString(prefSubtype: String): InputMethodSubtype? { - val elems = prefSubtype.split(Separators.SET) - if (elems.size != LENGTH_WITHOUT_EXTRA_VALUE && elems.size != LENGTH_WITH_EXTRA_VALUE) { - Log.w(TAG, "Unknown additional subtype specified: $prefSubtype") - return null - } - val languageTag = elems[INDEX_OF_LANGUAGE_TAG] - val locale = languageTag.constructLocale() - val keyboardLayoutSet = elems[INDEX_OF_KEYBOARD_LAYOUT] - val asciiCapable = locale.script() == ScriptUtils.SCRIPT_LATIN - // Here we assume that all the additional subtypes are EmojiCapable - val subtype = createEmojiCapableAdditionalSubtype(locale, keyboardLayoutSet, asciiCapable) - if (subtype.nameResId == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT && !isCustomLayout(keyboardLayoutSet)) { - // Skip unknown keyboard layout subtype. This may happen when predefined keyboard - // layout has been removed. - return null - } - return subtype - } - - fun createPrefSubtypes(subtypes: Array?): String { - if (subtypes.isNullOrEmpty()) { + fun createPrefSubtypes(subtypes: Collection): String { + if (subtypes.isEmpty()) return "" - } - val sb = StringBuilder() - for (subtype in subtypes) { - if (sb.isNotEmpty()) { - sb.append(Separators.SETS) - } - sb.append(getPrefSubtype(subtype)) - } - return sb.toString() - } - - fun createPrefSubtypes(prefSubtypes: Array?): String { - if (prefSubtypes.isNullOrEmpty()) { - return "" - } - val sb = StringBuilder() - for (prefSubtype in prefSubtypes) { - if (sb.isNotEmpty()) { - sb.append(Separators.SETS) - } - sb.append(prefSubtype) - } - return sb.toString() - } - - /** - * Returns the extra value that is optimized for the running OS. - * - * - * Historically the extra value has been used as the last resort to annotate various kinds of - * attributes. Some of these attributes are valid only on some platform versions. Thus we cannot - * assume that the extra values stored in a persistent storage are always valid. We need to - * regenerate the extra value on the fly instead. - * - * @param keyboardLayoutSetName the keyboard layout set name (e.g., "dvorak"). - * @param isAsciiCapable true when ASCII characters are supported with this layout. - * @param isEmojiCapable true when Unicode Emoji characters are supported with this layout. - * @return extra value that is optimized for the running OS. - * @see .getPlatformVersionIndependentSubtypeId - */ - private fun getPlatformVersionDependentExtraValue(locale: Locale, - keyboardLayoutSetName: String, isAsciiCapable: Boolean, isEmojiCapable: Boolean - ): String { - val extraValueItems = mutableListOf() - extraValueItems.add(ExtraValue.KEYBOARD_LAYOUT_SET + "=MAIN:" + keyboardLayoutSetName) - if (isAsciiCapable) { - extraValueItems.add(ExtraValue.ASCII_CAPABLE) - } - if (SubtypeLocaleUtils.isExceptionalLocale(locale)) { - extraValueItems.add( - ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + - SubtypeLocaleUtils.getMainLayoutDisplayName(keyboardLayoutSetName) - ) - } - if (isEmojiCapable) { - extraValueItems.add(ExtraValue.EMOJI_CAPABLE) - } - extraValueItems.add(ExtraValue.IS_ADDITIONAL_SUBTYPE) - return extraValueItems.joinToString(",") + return subtypes.joinToString(Separators.SETS) { it.toSettingsSubtype().toPref() } } /** * Returns the subtype ID that is supposed to be compatible between different version of OSes. * - * * From the compatibility point of view, it is important to keep subtype id predictable and * stable between different OSes. For this purpose, the calculation code in this method is * carefully chosen and then fixed. Treat the following code as no more or less than a @@ -188,43 +90,45 @@ object SubtypeUtilsAdditional { * For example, you don't need to update `compatibilityExtraValueItems` in this * method even when we need to add some new extra values for the actual instance of * [InputMethodSubtype]. - * - * @param keyboardLayoutSetName the keyboard layout set name (e.g., "dvorak"). - * @return a platform-version independent subtype ID. - * @see .getPlatformVersionDependentExtraValue */ - private fun getPlatformVersionIndependentSubtypeId(locale: Locale, keyboardLayoutSetName: String): Int { - // For compatibility reasons, we concatenate the extra values in the following order. - // - KeyboardLayoutSet - // - AsciiCapable - // - UntranslatableReplacementStringInSubtypeName - // - EmojiCapable - // - isAdditionalSubtype - val compatibilityExtraValueItems = mutableListOf() - compatibilityExtraValueItems.add(ExtraValue.KEYBOARD_LAYOUT_SET + "=MAIN:" + keyboardLayoutSetName) - compatibilityExtraValueItems.add(ExtraValue.ASCII_CAPABLE) - if (SubtypeLocaleUtils.isExceptionalLocale(locale)) { - compatibilityExtraValueItems.add( - ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + - SubtypeLocaleUtils.getMainLayoutDisplayName(keyboardLayoutSetName) - ) - } - compatibilityExtraValueItems.add(ExtraValue.EMOJI_CAPABLE) - compatibilityExtraValueItems.add(ExtraValue.IS_ADDITIONAL_SUBTYPE) - val compatibilityExtraValues = compatibilityExtraValueItems.joinToString(",") + private fun getSubtypeId(locale: Locale, extraValue: String, asciiCapable: Boolean): Int { + // basically we use the hashCode as specified for id in https://developer.android.com/reference/android/view/inputmethod/InputMethodSubtype return arrayOf( locale, Constants.Subtype.KEYBOARD_MODE, - compatibilityExtraValues, + extraValue, false, // isAuxiliary - false // overrideImplicitlyEnabledSubtype + false, // overrideImplicitlyEnabledSubtype + asciiCapable // asciiCapable ).contentHashCode() } - private val TAG: String = SubtypeUtilsAdditional::class.java.simpleName - private const val INDEX_OF_LANGUAGE_TAG: Int = 0 - private const val INDEX_OF_KEYBOARD_LAYOUT: Int = 1 - private const val INDEX_OF_EXTRA_VALUE: Int = 2 - private const val LENGTH_WITHOUT_EXTRA_VALUE: Int = (INDEX_OF_KEYBOARD_LAYOUT + 1) - private const val LENGTH_WITH_EXTRA_VALUE: Int = (INDEX_OF_EXTRA_VALUE + 1) + /** + * Returns the extra value that is optimized for the running OS. + * + * Historically the extra value has been used as the last resort to annotate various kinds of + * attributes. Some of these attributes are valid only on some platform versions. Thus we cannot + * assume that the extra values stored in a persistent storage are always valid. We need to + * regenerate the extra value on the fly instead. + * + * @param mainLayoutName the keyboard main layout name (e.g., "dvorak"). + * @param isAsciiCapable true when ASCII characters are supported with this layout. + * @param isEmojiCapable true when Unicode Emoji characters are supported with this layout. + * @return extra value that is optimized for the running OS. + * @see .getPlatformVersionIndependentSubtypeId + */ + private fun getAdditionalExtraValues(locale: Locale, mainLayoutName: String, isAsciiCapable: Boolean, isEmojiCapable: Boolean): String { + val extraValueItems = mutableListOf() + if (isAsciiCapable) + extraValueItems.add(ExtraValue.ASCII_CAPABLE) + if (SubtypeLocaleUtils.isExceptionalLocale(locale)) { + extraValueItems.add( + ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + SubtypeLocaleUtils.getMainLayoutDisplayName(mainLayoutName) + ) + } + if (isEmojiCapable) + extraValueItems.add(ExtraValue.EMOJI_CAPABLE) + extraValueItems.add(ExtraValue.IS_ADDITIONAL_SUBTYPE) + return extraValueItems.joinToString(",") + } } 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 6f77a47ca..f0b5241a2 100644 --- a/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutEditDialog.kt +++ b/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutEditDialog.kt @@ -44,14 +44,14 @@ fun LayoutEditDialog( val startIsCustom = LayoutUtilsCustom.isCustomLayout(initialLayoutName) var displayNameValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue( - if (startIsCustom) LayoutUtilsCustom.getSecondaryLayoutDisplayName(initialLayoutName) + if (startIsCustom) LayoutUtilsCustom.getDisplayName(initialLayoutName) else initialLayoutName.getStringResourceOrName("layout_", ctx) )) } val nameValid = displayNameValue.text.isNotBlank() && ( - (startIsCustom && LayoutUtilsCustom.getLayoutName(displayNameValue.text) == initialLayoutName) - || isNameValid(LayoutUtilsCustom.getLayoutName(displayNameValue.text)) + (startIsCustom && LayoutUtilsCustom.getSecondaryLayoutName(displayNameValue.text) == initialLayoutName) + || isNameValid(LayoutUtilsCustom.getSecondaryLayoutName(displayNameValue.text)) ) TextInputDialog( @@ -60,7 +60,7 @@ fun LayoutEditDialog( onDismissRequest() }, onConfirmed = { - val newLayoutName = LayoutUtilsCustom.getLayoutName(displayNameValue.text) + val newLayoutName = LayoutUtilsCustom.getSecondaryLayoutName(displayNameValue.text) if (startIsCustom && initialLayoutName != newLayoutName) LayoutUtilsCustom.getLayoutFile(initialLayoutName, layoutType, ctx).delete() LayoutUtilsCustom.getLayoutFile(newLayoutName, layoutType, ctx).writeText(it) 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 81767081f..695369439 100644 --- a/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutPickerDialog.kt +++ b/app/src/main/java/helium314/keyboard/settings/dialogs/LayoutPickerDialog.kt @@ -167,7 +167,7 @@ private fun AddLayoutRow(onNewLayout: (String) -> Unit, userLayouts: Collection< singleLine = true ) IconButton( - enabled = textValue.text.isNotEmpty() && LayoutUtilsCustom.getLayoutName(textValue.text) !in userLayouts, + enabled = textValue.text.isNotEmpty() && LayoutUtilsCustom.getSecondaryLayoutName(textValue.text) !in userLayouts, onClick = { onNewLayout(textValue.text) } ) { Icon(painterResource(R.drawable.ic_edit), null) } } @@ -206,7 +206,7 @@ private fun LayoutItemRow( } ) Text( - text = if (isCustom) LayoutUtilsCustom.getSecondaryLayoutDisplayName(layoutName) + text = if (isCustom) LayoutUtilsCustom.getDisplayName(layoutName) else layoutName.getStringResourceOrName("layout_", ctx), style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f), @@ -219,7 +219,7 @@ private fun LayoutItemRow( if (showDeleteDialog) ConfirmationDialog( onDismissRequest = { showDeleteDialog = false }, - text = { Text(stringResource(R.string.delete_layout, LayoutUtilsCustom.getSecondaryLayoutDisplayName(layoutName))) }, + text = { Text(stringResource(R.string.delete_layout, LayoutUtilsCustom.getDisplayName(layoutName))) }, confirmButtonText = stringResource(R.string.delete), onConfirmed = { showDeleteDialog = false diff --git a/app/src/main/java/helium314/keyboard/settings/screens/LayoutScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/LayoutScreen.kt index b9a20edd8..3ea18eb66 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/LayoutScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/LayoutScreen.kt @@ -50,7 +50,7 @@ fun createLayoutSettings(context: Context) = listOf( Log.v("irrelevant", "stupid way to trigger recomposition on preference change") var showDialog by rememberSaveable { mutableStateOf(false) } val currentLayout = Settings.readDefaultLayoutName(layoutType, prefs) - val displayName = if (LayoutUtilsCustom.isCustomLayout(currentLayout)) LayoutUtilsCustom.getSecondaryLayoutDisplayName(currentLayout) + val displayName = if (LayoutUtilsCustom.isCustomLayout(currentLayout)) LayoutUtilsCustom.getDisplayName(currentLayout) else currentLayout.getStringResourceOrName("layout_", ctx) Preference( name = setting.title,