store layouts in the same way in selected, enabled and additional layouts

This commit is contained in:
Helium314 2025-02-16 10:44:11 +01:00
parent d005ffac06
commit a25ed6d5e0
16 changed files with 272 additions and 296 deletions

View file

@ -13,8 +13,8 @@ android {
applicationId = "helium314.keyboard" applicationId = "helium314.keyboard"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 2306 versionCode = 2307
versionName = "2.3+dev5" versionName = "2.3+dev6"
ndk { ndk {
abiFilters.clear() abiFilters.clear()
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")) abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64"))

View file

@ -9,6 +9,7 @@ import helium314.keyboard.keyboard.KeyboardTheme
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode.checkAndConvertCode
import helium314.keyboard.latin.common.ColorType import helium314.keyboard.latin.common.ColorType
import helium314.keyboard.latin.common.Constants.Separators 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.LocaleUtils.constructLocale
import helium314.keyboard.latin.common.encodeBase36 import helium314.keyboard.latin.common.encodeBase36
import helium314.keyboard.latin.settings.Defaults 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.LayoutUtilsCustom
import helium314.keyboard.latin.utils.ScriptUtils.SCRIPT_LATIN import helium314.keyboard.latin.utils.ScriptUtils.SCRIPT_LATIN
import helium314.keyboard.latin.utils.ScriptUtils.script 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.ToolbarKey
import helium314.keyboard.latin.utils.defaultPinnedToolbarPref 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.prefs
import helium314.keyboard.latin.utils.protectedPrefs import helium314.keyboard.latin.utils.protectedPrefs
import helium314.keyboard.latin.utils.upgradeToolbarPrefs import helium314.keyboard.latin.utils.upgradeToolbarPrefs
@ -361,7 +368,7 @@ fun checkVersionUpgrade(context: Context) {
if (locale.script() != SCRIPT_LATIN) return@forEach if (locale.script() != SCRIPT_LATIN) return@forEach
// change language tag to SCRIPT_LATIN, but // change language tag to SCRIPT_LATIN, but
// avoid overwriting if 2 layouts have a different language tag, but the same name // 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 newFile = File(it.parentFile!!, LayoutUtilsCustom.getMainLayoutName(layoutDisplayName, locale))
var i = 1 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 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() 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) upgradeToolbarPrefs(prefs)
LayoutUtilsCustom.onLayoutFileChanged() // just to be sure LayoutUtilsCustom.onLayoutFileChanged() // just to be sure
prefs.edit { putInt(Settings.PREF_VERSION_CODE, BuildConfig.VERSION_CODE) } prefs.edit { putInt(Settings.PREF_VERSION_CODE, BuildConfig.VERSION_CODE) }

View file

@ -8,10 +8,10 @@ package helium314.keyboard.latin;
import android.os.Build; import android.os.Build;
import android.text.InputType; import android.text.InputType;
import helium314.keyboard.latin.utils.Log;
import android.view.inputmethod.EditorInfo; 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 helium314.keyboard.latin.utils.InputTypeUtils;
import java.util.ArrayList; import java.util.ArrayList;
@ -256,10 +256,9 @@ public final class InputAttributes {
mTargetApplicationPackageName); mTargetApplicationPackageName);
} }
public static boolean inPrivateImeOptions(final String packageName, final String key, public static boolean inPrivateImeOptions(final String packageName, final String key, final EditorInfo editorInfo) {
final EditorInfo editorInfo) {
if (editorInfo == null) return false; if (editorInfo == null) return false;
final String findingKey = (packageName != null) ? packageName + "." + key : key; final String findingKey = (packageName != null) ? packageName + "." + key : key;
return StringUtils.containsInCommaSplittableText(findingKey, editorInfo.privateImeOptions); return StringUtilsKt.containsValueWhenSplit(editorInfo.privateImeOptions, findingKey, ",");
} }
} }

View file

@ -123,51 +123,6 @@ public final class StringUtils {
return new String(Character.toChars(codePoint)); 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<String> 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. * Remove duplicates from an array of strings.
* <p> * <p>

View file

@ -129,6 +129,11 @@ fun encodeBase36(string: String): String = BigInteger(string.toByteArray()).toSt
fun decodeBase36(string: String) = BigInteger(string, 36).toByteArray().decodeToString() 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(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c))
fun isEmoji(s: CharSequence): Boolean = mightBeEmoji(s) && s.matches(emoRegex) fun isEmoji(s: CharSequence): Boolean = mightBeEmoji(s) && s.matches(emoRegex)

View file

@ -209,7 +209,7 @@ class LanguageSettingsDialog(
reloadSetting() reloadSetting()
} }
if (isCustom) { 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 { } else {
delete() delete()
} }

View file

@ -199,8 +199,8 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
editorInfo.inputType = InputType.TYPE_CLASS_TEXT; editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
Settings.getInstance().loadSettings(this, locale, new InputAttributes(editorInfo, false, getPackageName())); Settings.getInstance().loadSettings(this, locale, new InputAttributes(editorInfo, false, getPackageName()));
} }
final String keyboardLayoutName = SubtypeSettingsKt.getMatchingLayoutSetNameForLocale(locale); final String mainLayoutName = SubtypeSettingsKt.getMatchingMainLayoutNameForLocale(locale);
final InputMethodSubtype subtype = SubtypeUtilsAdditional.INSTANCE.createDummyAdditionalSubtype(locale, keyboardLayoutName); final InputMethodSubtype subtype = SubtypeUtilsAdditional.INSTANCE.createDummyAdditionalSubtype(locale, mainLayoutName);
final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype); final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype);
return keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); return keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET);
} }

View file

@ -2,6 +2,7 @@ package helium314.keyboard.latin.utils
import helium314.keyboard.latin.R import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.Constants.Separators import helium314.keyboard.latin.common.Constants.Separators
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue
import java.io.File import java.io.File
import java.util.EnumMap import java.util.EnumMap
@ -12,9 +13,9 @@ enum class LayoutType {
companion object { companion object {
fun EnumMap<LayoutType, String>.toExtraValue() = map { it.key.name + Separators.KV + it.value }.joinToString(Separators.ENTRY) fun EnumMap<LayoutType, String>.toExtraValue() = map { it.key.name + Separators.KV + it.value }.joinToString(Separators.ENTRY)
fun getLayoutMap(extraValue: String): EnumMap<LayoutType, String> { fun getLayoutMap(string: String): EnumMap<LayoutType, String> {
val map = EnumMap<LayoutType, String>(LayoutType::class.java) val map = EnumMap<LayoutType, String>(LayoutType::class.java)
extraValue.split(Separators.ENTRY).forEach { string.split(Separators.ENTRY).forEach {
val s = it.split(Separators.KV) val s = it.split(Separators.KV)
runCatching { map[LayoutType.valueOf(s[0])] = s[1] } runCatching { map[LayoutType.valueOf(s[0])] = s[1] }
} }
@ -37,5 +38,12 @@ enum class LayoutType {
EMOJI_BOTTOM -> R.string.layout_emoji_bottom_row EMOJI_BOTTOM -> R.string.layout_emoji_bottom_row
CLIPBOARD_BOTTOM -> R.string.layout_clip_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]
}
} }
} }

View file

@ -166,7 +166,7 @@ object LayoutUtilsCustom {
customLayoutMap.clear() customLayoutMap.clear()
} }
fun getSecondaryLayoutDisplayName(layoutName: String) = fun getDisplayName(layoutName: String) =
try { try {
if (layoutName.count { it == '.' } == 3) // main layout: "custom.<locale or script>.<name>.", other: custom.<name>. if (layoutName.count { it == '.' } == 3) // main layout: "custom.<locale or script>.<name>.", other: custom.<name>.
decodeBase36(layoutName.substringAfter(CUSTOM_LAYOUT_PREFIX).substringAfter(".").substringBeforeLast(".")) decodeBase36(layoutName.substringAfter(CUSTOM_LAYOUT_PREFIX).substringAfter(".").substringBeforeLast("."))
@ -175,7 +175,7 @@ object LayoutUtilsCustom {
layoutName 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) = fun getMainLayoutName(displayName: String, locale: Locale) =
if (locale.script() == ScriptUtils.SCRIPT_LATIN) if (locale.script() == ScriptUtils.SCRIPT_LATIN)
@ -196,7 +196,7 @@ object LayoutUtilsCustom {
setText(startContent ?: file.readText()) setText(startContent ?: file.readText())
} }
val builder = AlertDialog.Builder(context) val builder = AlertDialog.Builder(context)
.setTitle(getSecondaryLayoutDisplayName(layoutName)) .setTitle(getDisplayName(layoutName))
.setView(editText) .setView(editText)
.setPositiveButton(R.string.save) { _, _ -> .setPositiveButton(R.string.save) { _, _ ->
val content = editText.text.toString() val content = editText.text.toString()

View file

@ -267,7 +267,7 @@ public final class SubtypeLocaleUtils {
@Nullable @Nullable
public static String getMainLayoutDisplayName(@NonNull final String layoutName) { public static String getMainLayoutDisplayName(@NonNull final String layoutName) {
if (LayoutUtilsCustom.INSTANCE.isCustomLayout(layoutName)) if (LayoutUtilsCustom.INSTANCE.isCustomLayout(layoutName))
return LayoutUtilsCustom.INSTANCE.getSecondaryLayoutDisplayName(layoutName); return LayoutUtilsCustom.INSTANCE.getDisplayName(layoutName);
return sKeyboardLayoutToDisplayNameMap.get(layoutName); return sKeyboardLayoutToDisplayNameMap.get(layoutName);
} }

View file

@ -5,22 +5,19 @@ package helium314.keyboard.latin.utils
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources import android.content.res.Resources
import android.os.Build
import android.view.inputmethod.InputMethodSubtype import android.view.inputmethod.InputMethodSubtype
import android.widget.Toast import android.widget.Toast
import androidx.core.app.LocaleManagerCompat import androidx.core.app.LocaleManagerCompat
import androidx.core.content.edit import androidx.core.content.edit
import helium314.keyboard.keyboard.KeyboardSwitcher import helium314.keyboard.keyboard.KeyboardSwitcher
import helium314.keyboard.latin.R
import helium314.keyboard.latin.RichInputMethodManager import helium314.keyboard.latin.RichInputMethodManager
import helium314.keyboard.latin.common.Constants.Separators import helium314.keyboard.latin.common.Constants.Separators
import helium314.keyboard.latin.common.LocaleUtils import helium314.keyboard.latin.common.LocaleUtils
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.define.DebugFlags import helium314.keyboard.latin.define.DebugFlags
import helium314.keyboard.latin.settings.Defaults import helium314.keyboard.latin.settings.Defaults
import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.ScriptUtils.script import helium314.keyboard.latin.utils.ScriptUtils.script
import org.xmlpull.v1.XmlPullParser import helium314.keyboard.latin.utils.SettingsSubtype.Companion.toSettingsSubtype
import java.util.Locale import java.util.Locale
/** @return enabled subtypes. If no subtypes are enabled, but a contextForFallback is provided, /** @return enabled subtypes. If no subtypes are enabled, but a contextForFallback is provided,
@ -39,7 +36,7 @@ fun getAllAvailableSubtypes(): List<InputMethodSubtype> {
return resourceSubtypesByLocale.values.flatten() + additionalSubtypes return resourceSubtypesByLocale.values.flatten() + additionalSubtypes
} }
fun getMatchingLayoutSetNameForLocale(locale: Locale): String { fun getMatchingMainLayoutNameForLocale(locale: Locale): String {
val subtypes = resourceSubtypesByLocale.values.flatten() val subtypes = resourceSubtypesByLocale.values.flatten()
val name = LocaleUtils.getBestMatch(locale, subtypes) { it.locale() }?.mainLayoutName() val name = LocaleUtils.getBestMatch(locale, subtypes) { it.locale() }?.mainLayoutName()
if (name != null) return name if (name != null) return name
@ -57,7 +54,7 @@ fun getMatchingLayoutSetNameForLocale(locale: Locale): String {
fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) { fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) {
require(initialized) 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 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) val newString = (oldSubtypeStrings + subtypeString).filter { it.isNotBlank() }.toSortedSet().joinToString(Separators.SETS)
prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newString) } 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 */ /** returns whether subtype was actually removed, does not remove last subtype */
fun removeEnabledSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) { fun removeEnabledSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) {
require(initialized) require(initialized)
removeEnabledSubtype(prefs, subtype.prefString()) removeEnabledSubtype(prefs, subtype.toSettingsSubtype().toPref())
enabledSubtypes.remove(subtype) enabledSubtypes.remove(subtype)
RichInputMethodManager.getInstance().refreshSubtypeCaches() RichInputMethodManager.getInstance().refreshSubtypeCaches()
} }
fun getSelectedSubtype(prefs: SharedPreferences): InputMethodSubtype { fun getSelectedSubtype(prefs: SharedPreferences): InputMethodSubtype {
require(initialized) 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() val subtypes = if (prefs.getBoolean(Settings.PREF_USE_SYSTEM_LOCALES, Defaults.PREF_USE_SYSTEM_LOCALES)) getDefaultEnabledSubtypes()
else enabledSubtypes 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) { if (subtype != null) {
return subtype return subtype
} else { } 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()) if (subtypes.isNotEmpty())
return subtypes.first() return subtypes.first()
val defaultSubtypes = getDefaultEnabledSubtypes() val defaultSubtypes = getDefaultEnabledSubtypes()
return defaultSubtypes.firstOrNull { localeAndLayout.first == it.locale() && localeAndLayout.second == SubtypeLocaleUtils.getMainLayoutName(it) } return defaultSubtypes.firstOrNull { it.locale() == selectedSubtype.locale && it.mainLayoutName() == it.mainLayoutName() }
?: defaultSubtypes.firstOrNull { localeAndLayout.first.language == it.locale().language && localeAndLayout.second == SubtypeLocaleUtils.getMainLayoutName(it) } ?: defaultSubtypes.firstOrNull { it.locale().language == selectedSubtype.locale.language }
?: defaultSubtypes.first() ?: defaultSubtypes.first()
} }
fun setSelectedSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) { 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) if (subtype.locale().toLanguageTag().isEmpty() || prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE) == subtypeString)
return return
prefs.edit { putString(Settings.PREF_SELECTED_SUBTYPE, subtypeString) } prefs.edit { putString(Settings.PREF_SELECTED_SUBTYPE, subtypeString) }
@ -180,52 +182,9 @@ private fun getDefaultEnabledSubtypes(): List<InputMethodSubtype> {
return systemSubtypes 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<Locale, String> =
substringBefore(Separators.SET).constructLocale() to substringAfter(Separators.SET)
private fun Pair<Locale, String>.prefString() =
first.toLanguageTag() + Separators.SET + second
private fun loadResourceSubtypes(resources: Resources) { private fun loadResourceSubtypes(resources: Resources) {
val xml = resources.getXml(R.xml.method) getResourceSubtypes(resources).forEach {
xml.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true) resourceSubtypesByLocale.getOrPut(it.locale()) { ArrayList(2) }.add(it)
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()
} }
} }
@ -255,30 +214,33 @@ private fun loadAdditionalSubtypes(prefs: SharedPreferences) {
// requires loadResourceSubtypes to be called before // requires loadResourceSubtypes to be called before
private fun loadEnabledSubtypes(context: Context) { private fun loadEnabledSubtypes(context: Context) {
val prefs = context.prefs() val prefs = context.prefs()
val subtypeStrings = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!! val settingsSubtypes = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!
.split(Separators.SETS).filter { it.isNotEmpty() }.map { it.toLocaleAndLayout() } .split(Separators.SETS).filter { it.isNotEmpty() }.map { it.toSettingsSubtype() }
for (settingsSubtype in settingsSubtypes) {
for (localeAndLayout in subtypeStrings) { val additionalSubtype = settingsSubtype.toAdditionalSubtype()
val subtypesForLocale = resourceSubtypesByLocale[localeAndLayout.first] if (additionalSubtype != null && additionalSubtypes.contains(additionalSubtype)) {
enabledSubtypes.add(additionalSubtype)
continue
}
val subtypesForLocale = resourceSubtypesByLocale[settingsSubtype.locale]
if (subtypesForLocale == null) { if (subtypesForLocale == null) {
val message = "no resource subtype for $localeAndLayout" val message = "no resource subtype for $settingsSubtype"
Log.w(TAG, message) Log.w(TAG, message)
if (DebugFlags.DEBUG_ENABLED) if (DebugFlags.DEBUG_ENABLED)
Toast.makeText(context, message, Toast.LENGTH_LONG).show() Toast.makeText(context, message, Toast.LENGTH_LONG).show()
else // don't remove in debug mode else // don't remove in debug mode
removeEnabledSubtype(prefs, localeAndLayout.prefString()) removeEnabledSubtype(prefs, settingsSubtype.toPref())
continue continue
} }
val subtype = subtypesForLocale.firstOrNull { SubtypeLocaleUtils.getMainLayoutName(it) == localeAndLayout.second } val subtype = subtypesForLocale.firstOrNull { SubtypeLocaleUtils.getMainLayoutName(it) == (settingsSubtype.mainLayoutName() ?: "qwerty") }
?: additionalSubtypes.firstOrNull { it.locale() == localeAndLayout.first && SubtypeLocaleUtils.getMainLayoutName(it) == localeAndLayout.second }
if (subtype == null) { if (subtype == null) {
val message = "subtype $localeAndLayout could not be loaded" val message = "subtype $settingsSubtype could not be loaded"
Log.w(TAG, message) Log.w(TAG, message)
if (DebugFlags.DEBUG_ENABLED) if (DebugFlags.DEBUG_ENABLED)
Toast.makeText(context, message, Toast.LENGTH_LONG).show() Toast.makeText(context, message, Toast.LENGTH_LONG).show()
else // don't remove in debug mode else // don't remove in debug mode
removeEnabledSubtype(prefs, localeAndLayout.prefString()) removeEnabledSubtype(prefs, settingsSubtype.toPref())
continue continue
} }
@ -296,7 +258,7 @@ private fun removeEnabledSubtype(prefs: SharedPreferences, subtypeString: String
// switch subtype if the currently used one has been disabled // switch subtype if the currently used one has been disabled
try { try {
val nextSubtype = RichInputMethodManager.getInstance().getNextSubtypeInThisIme(true) val nextSubtype = RichInputMethodManager.getInstance().getNextSubtypeInThisIme(true)
if (subtypeString == nextSubtype?.prefString()) if (subtypeString == nextSubtype?.toSettingsSubtype()?.toPref())
KeyboardSwitcher.getInstance().switchToSubtype(getDefaultEnabledSubtypes().first()) KeyboardSwitcher.getInstance().switchToSubtype(getDefaultEnabledSubtypes().first())
else else
KeyboardSwitcher.getInstance().switchToSubtype(nextSubtype) KeyboardSwitcher.getInstance().switchToSubtype(nextSubtype)

View file

@ -1,11 +1,18 @@
package helium314.keyboard.latin.utils package helium314.keyboard.latin.utils
import android.content.Context import android.content.Context
import android.content.res.Resources
import android.os.Build import android.os.Build
import android.view.inputmethod.InputMethodSubtype 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.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET
import helium314.keyboard.latin.common.LocaleUtils import helium314.keyboard.latin.common.LocaleUtils
import helium314.keyboard.latin.common.LocaleUtils.constructLocale 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 import java.util.Locale
fun InputMethodSubtype.locale(): Locale { fun InputMethodSubtype.locale(): Locale {
@ -21,11 +28,92 @@ fun InputMethodSubtype.mainLayoutName(): String? {
return map[LayoutType.MAIN] return map[LayoutType.MAIN]
} }
fun getResourceSubtypes(resources: Resources): List<InputMethodSubtype> {
val subtypes = mutableListOf<InputMethodSubtype>()
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 */ /** Workaround for SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale ignoring custom layout names */
// todo (later): this should be done properly and in SubtypeLocaleUtils // todo (later): this should be done properly and in SubtypeLocaleUtils
fun InputMethodSubtype.displayName(context: Context): CharSequence { fun InputMethodSubtype.displayName(context: Context): CharSequence {
val layoutName = SubtypeLocaleUtils.getMainLayoutName(this) val layoutName = SubtypeLocaleUtils.getMainLayoutName(this)
if (LayoutUtilsCustom.isCustomLayout(layoutName)) if (LayoutUtilsCustom.isCustomLayout(layoutName))
return "${LocaleUtils.getLocaleDisplayNameInSystemLocale(locale(), context)} (${LayoutUtilsCustom.getSecondaryLayoutDisplayName(layoutName)})" return "${LocaleUtils.getLocaleDisplayNameInSystemLocale(locale(), context)} (${LayoutUtilsCustom.getDisplayName(layoutName)})"
return SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(this) 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)
}
}
}

View file

@ -2,60 +2,60 @@ package helium314.keyboard.latin.utils
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.text.TextUtils
import android.view.inputmethod.InputMethodSubtype import android.view.inputmethod.InputMethodSubtype
import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder
import helium314.keyboard.latin.R import helium314.keyboard.latin.R
import helium314.keyboard.latin.common.Constants import helium314.keyboard.latin.common.Constants
import helium314.keyboard.latin.common.Constants.Separators import helium314.keyboard.latin.common.Constants.Separators
import helium314.keyboard.latin.common.Constants.Subtype.ExtraValue 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.Defaults
import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.LayoutUtilsCustom.isCustomLayout import helium314.keyboard.latin.utils.SettingsSubtype.Companion.toSettingsSubtype
import helium314.keyboard.latin.utils.ScriptUtils.script
import java.util.Locale import java.util.Locale
object SubtypeUtilsAdditional { object SubtypeUtilsAdditional {
fun isAdditionalSubtype(subtype: InputMethodSubtype): Boolean { fun isAdditionalSubtype(subtype: InputMethodSubtype): Boolean {
return subtype.containsExtraValueKey(ExtraValue.IS_ADDITIONAL_SUBTYPE) return subtype.containsExtraValueKey(ExtraValue.IS_ADDITIONAL_SUBTYPE)
} }
private fun createAdditionalSubtypeInternal(locale: Locale, keyboardLayoutSetName: String, // todo: extra value does not contain UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME for custom layout
isAsciiCapable: Boolean, isEmojiCapable: Boolean // 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
): InputMethodSubtype { // see also todo in SettingsSubtype
val nameId = SubtypeLocaleUtils.getSubtypeNameResId(locale, keyboardLayoutSetName) fun createAdditionalSubtype(locale: Locale, extraValue: String, isAsciiCapable: Boolean,
val platformVersionDependentExtraValues = isEmojiCapable: Boolean): InputMethodSubtype {
getPlatformVersionDependentExtraValue(locale, keyboardLayoutSetName, isAsciiCapable, isEmojiCapable) val mainLayoutName = LayoutType.getMainLayoutFromExtraValue(extraValue) ?: "qwerty"
val platformVersionIndependentSubtypeId = val nameId = SubtypeLocaleUtils.getSubtypeNameResId(locale, mainLayoutName)
getPlatformVersionIndependentSubtypeId(locale, keyboardLayoutSetName) val fullExtraValue = extraValue + "," + getAdditionalExtraValues(locale, mainLayoutName, isAsciiCapable, isEmojiCapable)
val subtypeId = getSubtypeId(locale, fullExtraValue, isAsciiCapable)
val builder = InputMethodSubtypeBuilder() val builder = InputMethodSubtypeBuilder()
.setSubtypeNameResId(nameId) .setSubtypeNameResId(nameId)
.setSubtypeIconResId(R.drawable.ic_ime_switcher) .setSubtypeIconResId(R.drawable.ic_ime_switcher)
.setSubtypeLocale(locale.toString()) .setSubtypeLocale(locale.toString())
.setSubtypeMode(Constants.Subtype.KEYBOARD_MODE) .setSubtypeMode(Constants.Subtype.KEYBOARD_MODE)
.setSubtypeExtraValue(platformVersionDependentExtraValues) .setSubtypeExtraValue(fullExtraValue)
.setIsAuxiliary(false) .setIsAuxiliary(false)
.setOverridesImplicitlyEnabledSubtype(false) .setOverridesImplicitlyEnabledSubtype(false)
.setSubtypeId(platformVersionIndependentSubtypeId) .setSubtypeId(subtypeId)
.setIsAsciiCapable(isAsciiCapable) .setIsAsciiCapable(isAsciiCapable)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
builder.setLanguageTag(locale.toLanguageTag()) 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() return builder.build()
} }
fun createDummyAdditionalSubtype(locale: Locale, keyboardLayoutSetName: String) = fun createDummyAdditionalSubtype(locale: Locale, mainLayoutName: String) =
createAdditionalSubtypeInternal(locale, keyboardLayoutSetName, false, false) createAdditionalSubtype(locale, "MAIN${Separators.KV}$mainLayoutName", false, false)
fun createEmojiCapableAdditionalSubtype(locale: Locale, keyboardLayoutSetName: String, asciiCapable: Boolean) = fun createEmojiCapableAdditionalSubtype(locale: Locale, mainLayoutName: String, asciiCapable: Boolean) =
createAdditionalSubtypeInternal(locale, keyboardLayoutSetName, asciiCapable, true) createAdditionalSubtype(locale, "MAIN${Separators.KV}$mainLayoutName", asciiCapable, true)
fun addAdditionalSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) { fun addAdditionalSubtype(prefs: SharedPreferences, subtype: InputMethodSubtype) {
val oldAdditionalSubtypesString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!! val oldAdditionalSubtypesString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
val additionalSubtypes = createAdditionalSubtypes(oldAdditionalSubtypesString).toMutableSet() val additionalSubtypes = createAdditionalSubtypes(oldAdditionalSubtypesString).toMutableSet()
additionalSubtypes.add(subtype) additionalSubtypes.add(subtype)
val newAdditionalSubtypesString = createPrefSubtypes(additionalSubtypes.toTypedArray()) val newAdditionalSubtypesString = createPrefSubtypes(additionalSubtypes)
Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString) Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString)
} }
@ -63,123 +63,25 @@ object SubtypeUtilsAdditional {
val oldAdditionalSubtypesString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!! val oldAdditionalSubtypesString = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!
val oldAdditionalSubtypes = createAdditionalSubtypes(oldAdditionalSubtypesString) val oldAdditionalSubtypes = createAdditionalSubtypes(oldAdditionalSubtypesString)
val newAdditionalSubtypes = oldAdditionalSubtypes.filter { it != subtype } val newAdditionalSubtypes = oldAdditionalSubtypes.filter { it != subtype }
val newAdditionalSubtypesString = createPrefSubtypes(newAdditionalSubtypes.toTypedArray()) val newAdditionalSubtypesString = createPrefSubtypes(newAdditionalSubtypes)
Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString) 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<InputMethodSubtype> { fun createAdditionalSubtypes(prefSubtypes: String): List<InputMethodSubtype> {
if (TextUtils.isEmpty(prefSubtypes)) { if (prefSubtypes.isEmpty())
return emptyList() return emptyList()
} return prefSubtypes.split(Separators.SETS).mapNotNull { it.toSettingsSubtype().toAdditionalSubtype() }
return prefSubtypes.split(Separators.SETS)
.mapNotNull { createSubtypeFromString(it) }
} }
// use string created with getPrefSubtype fun createPrefSubtypes(subtypes: Collection<InputMethodSubtype>): String {
fun createSubtypeFromString(prefSubtype: String): InputMethodSubtype? { if (subtypes.isEmpty())
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<InputMethodSubtype>?): String {
if (subtypes.isNullOrEmpty()) {
return "" return ""
} return subtypes.joinToString(Separators.SETS) { it.toSettingsSubtype().toPref() }
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>?): 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<String>()
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(",")
} }
/** /**
* Returns the subtype ID that is supposed to be compatible between different version of OSes. * 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 * 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 * 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 * 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 * 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 * method even when we need to add some new extra values for the actual instance of
* [InputMethodSubtype]. * [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 { private fun getSubtypeId(locale: Locale, extraValue: String, asciiCapable: Boolean): Int {
// For compatibility reasons, we concatenate the extra values in the following order. // basically we use the hashCode as specified for id in https://developer.android.com/reference/android/view/inputmethod/InputMethodSubtype
// - KeyboardLayoutSet
// - AsciiCapable
// - UntranslatableReplacementStringInSubtypeName
// - EmojiCapable
// - isAdditionalSubtype
val compatibilityExtraValueItems = mutableListOf<String>()
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(",")
return arrayOf( return arrayOf(
locale, locale,
Constants.Subtype.KEYBOARD_MODE, Constants.Subtype.KEYBOARD_MODE,
compatibilityExtraValues, extraValue,
false, // isAuxiliary false, // isAuxiliary
false // overrideImplicitlyEnabledSubtype false, // overrideImplicitlyEnabledSubtype
asciiCapable // asciiCapable
).contentHashCode() ).contentHashCode()
} }
private val TAG: String = SubtypeUtilsAdditional::class.java.simpleName /**
private const val INDEX_OF_LANGUAGE_TAG: Int = 0 * Returns the extra value that is optimized for the running OS.
private const val INDEX_OF_KEYBOARD_LAYOUT: Int = 1 *
private const val INDEX_OF_EXTRA_VALUE: Int = 2 * Historically the extra value has been used as the last resort to annotate various kinds of
private const val LENGTH_WITHOUT_EXTRA_VALUE: Int = (INDEX_OF_KEYBOARD_LAYOUT + 1) * attributes. Some of these attributes are valid only on some platform versions. Thus we cannot
private const val LENGTH_WITH_EXTRA_VALUE: Int = (INDEX_OF_EXTRA_VALUE + 1) * 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<String>()
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(",")
}
} }

View file

@ -44,14 +44,14 @@ fun LayoutEditDialog(
val startIsCustom = LayoutUtilsCustom.isCustomLayout(initialLayoutName) val startIsCustom = LayoutUtilsCustom.isCustomLayout(initialLayoutName)
var displayNameValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { var displayNameValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue( mutableStateOf(TextFieldValue(
if (startIsCustom) LayoutUtilsCustom.getSecondaryLayoutDisplayName(initialLayoutName) if (startIsCustom) LayoutUtilsCustom.getDisplayName(initialLayoutName)
else initialLayoutName.getStringResourceOrName("layout_", ctx) else initialLayoutName.getStringResourceOrName("layout_", ctx)
)) ))
} }
val nameValid = displayNameValue.text.isNotBlank() val nameValid = displayNameValue.text.isNotBlank()
&& ( && (
(startIsCustom && LayoutUtilsCustom.getLayoutName(displayNameValue.text) == initialLayoutName) (startIsCustom && LayoutUtilsCustom.getSecondaryLayoutName(displayNameValue.text) == initialLayoutName)
|| isNameValid(LayoutUtilsCustom.getLayoutName(displayNameValue.text)) || isNameValid(LayoutUtilsCustom.getSecondaryLayoutName(displayNameValue.text))
) )
TextInputDialog( TextInputDialog(
@ -60,7 +60,7 @@ fun LayoutEditDialog(
onDismissRequest() onDismissRequest()
}, },
onConfirmed = { onConfirmed = {
val newLayoutName = LayoutUtilsCustom.getLayoutName(displayNameValue.text) val newLayoutName = LayoutUtilsCustom.getSecondaryLayoutName(displayNameValue.text)
if (startIsCustom && initialLayoutName != newLayoutName) if (startIsCustom && initialLayoutName != newLayoutName)
LayoutUtilsCustom.getLayoutFile(initialLayoutName, layoutType, ctx).delete() LayoutUtilsCustom.getLayoutFile(initialLayoutName, layoutType, ctx).delete()
LayoutUtilsCustom.getLayoutFile(newLayoutName, layoutType, ctx).writeText(it) LayoutUtilsCustom.getLayoutFile(newLayoutName, layoutType, ctx).writeText(it)

View file

@ -167,7 +167,7 @@ private fun AddLayoutRow(onNewLayout: (String) -> Unit, userLayouts: Collection<
singleLine = true singleLine = true
) )
IconButton( 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) } onClick = { onNewLayout(textValue.text) }
) { Icon(painterResource(R.drawable.ic_edit), null) } ) { Icon(painterResource(R.drawable.ic_edit), null) }
} }
@ -206,7 +206,7 @@ private fun LayoutItemRow(
} }
) )
Text( Text(
text = if (isCustom) LayoutUtilsCustom.getSecondaryLayoutDisplayName(layoutName) text = if (isCustom) LayoutUtilsCustom.getDisplayName(layoutName)
else layoutName.getStringResourceOrName("layout_", ctx), else layoutName.getStringResourceOrName("layout_", ctx),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -219,7 +219,7 @@ private fun LayoutItemRow(
if (showDeleteDialog) if (showDeleteDialog)
ConfirmationDialog( ConfirmationDialog(
onDismissRequest = { showDeleteDialog = false }, 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), confirmButtonText = stringResource(R.string.delete),
onConfirmed = { onConfirmed = {
showDeleteDialog = false showDeleteDialog = false

View file

@ -50,7 +50,7 @@ fun createLayoutSettings(context: Context) = listOf(
Log.v("irrelevant", "stupid way to trigger recomposition on preference change") Log.v("irrelevant", "stupid way to trigger recomposition on preference change")
var showDialog by rememberSaveable { mutableStateOf(false) } var showDialog by rememberSaveable { mutableStateOf(false) }
val currentLayout = Settings.readDefaultLayoutName(layoutType, prefs) 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) else currentLayout.getStringResourceOrName("layout_", ctx)
Preference( Preference(
name = setting.title, name = setting.title,