Merge branch 'Helium314:main' into inline-code-point-loops

This commit is contained in:
Devy Ballard 2025-03-11 17:59:33 -06:00 committed by GitHub
commit 84f73cc3da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 246 additions and 100 deletions

View file

@ -59,8 +59,7 @@ object LayoutParser {
/** Parse simple layouts, defined only as rows of (normal) keys with popup keys. */ /** Parse simple layouts, defined only as rows of (normal) keys with popup keys. */
fun parseSimpleString(layoutText: String): List<List<KeyData>> { fun parseSimpleString(layoutText: String): List<List<KeyData>> {
val rowStrings = layoutText.replace("\r\n", "\n").split("\\n\\s*\\n".toRegex()).filter { it.isNotBlank() } return LayoutUtils.getSimpleRowStrings(layoutText).map { row ->
return rowStrings.map { row ->
row.split("\n").mapNotNull { parseKey(it) } row.split("\n").mapNotNull { parseKey(it) }
} }
} }

View file

@ -161,7 +161,7 @@ fun checkVersionUpgrade(context: Context) {
split[1] = newName split[1] = newName
split.joinToString(":") split.joinToString(":")
} }
Settings.writePrefAdditionalSubtypes(prefs, newSubtypeStrings.joinToString(";")) prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, newSubtypeStrings.joinToString(";")).apply()
} }
// rename other custom layouts // rename other custom layouts
LayoutUtilsCustom.onLayoutFileChanged() LayoutUtilsCustom.onLayoutFileChanged()
@ -630,7 +630,7 @@ private fun upgradesWhenComingFromOldAppName(context: Context) {
val localeString = it.substringBefore(":") val localeString = it.substringBefore(":")
additionalSubtypes.add(it.replace(localeString, localeString.constructLocale().toLanguageTag())) additionalSubtypes.add(it.replace(localeString, localeString.constructLocale().toLanguageTag()))
} }
Settings.writePrefAdditionalSubtypes(prefs, additionalSubtypes.joinToString(";")) prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, additionalSubtypes.joinToString(";")).apply()
} }
// move pinned clips to credential protected storage if device is not locked (should never happen) // move pinned clips to credential protected storage if device is not locked (should never happen)
if (!prefs.contains(Settings.PREF_PINNED_CLIPS)) return if (!prefs.contains(Settings.PREF_PINNED_CLIPS)) return

View file

@ -523,6 +523,11 @@ public class LatinIME extends InputMethodService implements
} }
final class SubtypeState { final class SubtypeState {
// When HintLocales causes a subtype override, we store
// the overridden subtype here in order to restore it when
// we switch to another input context that has no HintLocales.
private InputMethodSubtype mOverriddenByLocale;
private InputMethodSubtype mLastActiveSubtype; private InputMethodSubtype mLastActiveSubtype;
private boolean mCurrentSubtypeHasBeenUsed = true; // starting with true avoids immediate switch private boolean mCurrentSubtypeHasBeenUsed = true; // starting with true avoids immediate switch
@ -530,6 +535,70 @@ public class LatinIME extends InputMethodService implements
mCurrentSubtypeHasBeenUsed = true; mCurrentSubtypeHasBeenUsed = true;
} }
// TextFields can provide locale/language hints that the IME should use via 'hintLocales'.
// If a matching subtype is found, we temporarily switch to that subtype until
// we return to a context that does not provide any hints, or until the user
// explicitly changes the language/subtype in use.
public InputMethodSubtype getSubtypeForLocales(final RichInputMethodManager richImm, final Iterable<Locale> locales) {
final InputMethodSubtype overriddenByLocale = mOverriddenByLocale;
if (locales == null) {
if (overriddenByLocale != null) {
// no locales provided, so switch back to
// whatever subtype was used last time.
mOverriddenByLocale = null;
return overriddenByLocale;
}
return null;
}
final InputMethodSubtype currentSubtype = richImm.getCurrentSubtype().getRawSubtype();
final Locale currentSubtypeLocale = richImm.getCurrentSubtypeLocale();
final int minimumMatchLevel = 3; // LocaleUtils.LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
// Try finding a subtype matching the hint language.
for (final Locale hintLocale : locales) {
if (LocaleUtils.INSTANCE.getMatchLevel(hintLocale, currentSubtypeLocale) >= minimumMatchLevel
|| CollectionsKt.any(mSettings.getCurrent().mSecondaryLocales,
(secLocale) -> LocaleUtils.INSTANCE.getMatchLevel(hintLocale, secLocale) >= minimumMatchLevel)) {
// current locales are already a good match, and we want to avoid unnecessary layout switches.
return null;
}
final InputMethodSubtype subtypeForHintLocale = richImm.findSubtypeForHintLocale(hintLocale);
if (subtypeForHintLocale == null) {
continue;
}
if (subtypeForHintLocale.equals(currentSubtype)) {
// no need to switch, we already use the correct locale.
return null;
}
if (overriddenByLocale == null) {
// auto-switching based on hint locale, so store
// whatever subtype was in use so we can switch back
// to it later when there are no hint locales.
mOverriddenByLocale = currentSubtype;
}
return subtypeForHintLocale;
}
return null;
}
public void onSubtypeChanged(final InputMethodSubtype oldSubtype,
final InputMethodSubtype newSubtype) {
if (oldSubtype != mOverriddenByLocale) {
// Whenever the subtype is changed, clear tracking
// the subtype that is overridden by a HintLocale as
// we no longer have a subtype to automatically switch back to.
mOverriddenByLocale = null;
}
}
public void switchSubtype(final RichInputMethodManager richImm) { public void switchSubtype(final RichInputMethodManager richImm) {
final InputMethodSubtype currentSubtype = richImm.getCurrentSubtype().getRawSubtype(); final InputMethodSubtype currentSubtype = richImm.getCurrentSubtype().getRawSubtype();
final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype; final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype;
@ -858,6 +927,8 @@ public class LatinIME extends InputMethodService implements
return; return;
} }
InputMethodSubtype oldSubtype = mRichImm.getCurrentSubtype().getRawSubtype(); InputMethodSubtype oldSubtype = mRichImm.getCurrentSubtype().getRawSubtype();
mSubtypeState.onSubtypeChanged(oldSubtype, subtype);
StatsUtils.onSubtypeChanged(oldSubtype, subtype); StatsUtils.onSubtypeChanged(oldSubtype, subtype);
mRichImm.onSubtypeChanged(subtype); mRichImm.onSubtypeChanged(subtype);
mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype), mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype),
@ -876,20 +947,10 @@ public class LatinIME extends InputMethodService implements
super.onStartInput(editorInfo, restarting); super.onStartInput(editorInfo, restarting);
final List<Locale> hintLocales = EditorInfoCompatUtils.getHintLocales(editorInfo); final List<Locale> hintLocales = EditorInfoCompatUtils.getHintLocales(editorInfo);
if (hintLocales == null) { final InputMethodSubtype subtypeForLocales = mSubtypeState.getSubtypeForLocales(mRichImm, hintLocales);
return; if (subtypeForLocales != null) {
} // found a better subtype using hint locales that we should switch to.
// Try switching to a subtype matching the hint language. mHandler.postSwitchLanguage(subtypeForLocales);
for (final Locale hintLocale : hintLocales) {
if (LocaleUtils.INSTANCE.getMatchLevel(hintLocale, mRichImm.getCurrentSubtypeLocale()) >= 3
|| CollectionsKt.any(mSettings.getCurrent().mSecondaryLocales, (secLocale) -> LocaleUtils.INSTANCE.getMatchLevel(hintLocale, secLocale) >= 3))
return; // current locales are already a good match, and we want to avoid unnecessary layout switches
final InputMethodSubtype newSubtype = mRichImm.findSubtypeForHintLocale(hintLocale);
if (newSubtype == null) continue;
if (newSubtype.equals(mRichImm.getCurrentSubtype().getRawSubtype()))
return; // no need to switch, we already use the correct locale
mHandler.postSwitchLanguage(newSubtype);
break;
} }
} }

View file

@ -9,7 +9,6 @@ import helium314.keyboard.keyboard.KeyboardTheme
import helium314.keyboard.latin.BuildConfig import helium314.keyboard.latin.BuildConfig
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.utils.JniUtils
import helium314.keyboard.latin.utils.LayoutType import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.POPUP_KEYS_LABEL_DEFAULT import helium314.keyboard.latin.utils.POPUP_KEYS_LABEL_DEFAULT
import helium314.keyboard.latin.utils.POPUP_KEYS_ORDER_DEFAULT import helium314.keyboard.latin.utils.POPUP_KEYS_ORDER_DEFAULT
@ -74,9 +73,9 @@ object Defaults {
const val PREF_LANGUAGE_SWITCH_KEY = "internal" const val PREF_LANGUAGE_SWITCH_KEY = "internal"
const val PREF_SHOW_EMOJI_KEY = false const val PREF_SHOW_EMOJI_KEY = false
const val PREF_VARIABLE_TOOLBAR_DIRECTION = true const val PREF_VARIABLE_TOOLBAR_DIRECTION = true
const val PREF_ADDITIONAL_SUBTYPES = "de${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=qwerty${Separators.SETS}" + const val PREF_ADDITIONAL_SUBTYPES = "de${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwerty${Separators.SETS}" +
"fr${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=qwertz${Separators.SETS}" + "fr${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwertz${Separators.SETS}" +
"hu${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=qwerty" "hu${Separators.SET}${ExtraValue.KEYBOARD_LAYOUT_SET}=MAIN:qwerty"
const val PREF_ENABLE_SPLIT_KEYBOARD = false const val PREF_ENABLE_SPLIT_KEYBOARD = false
const val PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE = false const val PREF_ENABLE_SPLIT_KEYBOARD_LANDSCAPE = false
const val PREF_SPLIT_SPACER_SCALE = SettingsValues.DEFAULT_SIZE_SCALE const val PREF_SPLIT_SPACER_SCALE = SettingsValues.DEFAULT_SIZE_SCALE
@ -152,8 +151,6 @@ object Defaults {
const val PREF_EMOJI_RECENT_KEYS = "" const val PREF_EMOJI_RECENT_KEYS = ""
const val PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID = 0 const val PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID = 0
const val PREF_PINNED_CLIPS = "" const val PREF_PINNED_CLIPS = ""
@JvmField
val PREF_LIBRARY_CHECKSUM: String = JniUtils.expectedDefaultChecksum()
const val PREF_SHOW_DEBUG_SETTINGS = false const val PREF_SHOW_DEBUG_SETTINGS = false
val PREF_DEBUG_MODE = BuildConfig.DEBUG val PREF_DEBUG_MODE = BuildConfig.DEBUG
const val PREF_SHOW_SUGGESTION_INFOS = false const val PREF_SHOW_SUGGESTION_INFOS = false

View file

@ -312,10 +312,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
mPrefs.edit().putBoolean(Settings.PREF_ALWAYS_INCOGNITO_MODE, !oldValue).apply(); mPrefs.edit().putBoolean(Settings.PREF_ALWAYS_INCOGNITO_MODE, !oldValue).apply();
} }
public static void writePrefAdditionalSubtypes(final SharedPreferences prefs, final String prefSubtypes) {
prefs.edit().putString(PREF_ADDITIONAL_SUBTYPES, prefSubtypes).apply();
}
public static int readHorizontalSpaceSwipe(final SharedPreferences prefs) { public static int readHorizontalSpaceSwipe(final SharedPreferences prefs) {
return switch (prefs.getString(PREF_SPACE_HORIZONTAL_SWIPE, Defaults.PREF_SPACE_HORIZONTAL_SWIPE)) { return switch (prefs.getString(PREF_SPACE_HORIZONTAL_SWIPE, Defaults.PREF_SPACE_HORIZONTAL_SWIPE)) {
case "move_cursor" -> KeyboardActionListener.SWIPE_MOVE_CURSOR; case "move_cursor" -> KeyboardActionListener.SWIPE_MOVE_CURSOR;

View file

@ -11,11 +11,9 @@ import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.define.DebugFlags import helium314.keyboard.latin.define.DebugFlags
import helium314.keyboard.latin.utils.LayoutType import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.LayoutType.Companion.toExtraValue import helium314.keyboard.latin.utils.LayoutType.Companion.toExtraValue
import helium314.keyboard.latin.utils.LayoutUtilsCustom
import helium314.keyboard.latin.utils.Log import helium314.keyboard.latin.utils.Log
import helium314.keyboard.latin.utils.ScriptUtils import helium314.keyboard.latin.utils.ScriptUtils
import helium314.keyboard.latin.utils.ScriptUtils.script import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.SubtypeLocaleUtils
import helium314.keyboard.latin.utils.SubtypeUtilsAdditional import helium314.keyboard.latin.utils.SubtypeUtilsAdditional
import helium314.keyboard.latin.utils.locale import helium314.keyboard.latin.utils.locale
import java.util.Locale import java.util.Locale
@ -27,23 +25,9 @@ data class SettingsSubtype(val locale: Locale, val extraValues: String) {
/** Creates an additional subtype from the SettingsSubtype. /** Creates an additional subtype from the SettingsSubtype.
* Resulting InputMethodSubtypes are equal if SettingsSubtypes are equal */ * Resulting InputMethodSubtypes are equal if SettingsSubtypes are equal */
fun toAdditionalSubtype(): InputMethodSubtype? { fun toAdditionalSubtype(): InputMethodSubtype {
val asciiCapable = locale.script() == ScriptUtils.SCRIPT_LATIN val asciiCapable = locale.script() == ScriptUtils.SCRIPT_LATIN
val subtype = SubtypeUtilsAdditional.createAdditionalSubtype(locale, extraValues, asciiCapable, true) return SubtypeUtilsAdditional.createAdditionalSubtype(locale, extraValues, asciiCapable, true)
// todo: this is returns null for all non-latin layouts
// either fix it, or remove the check
// if removed, removing a layout will result in fallback qwerty even for non-ascii, but this is better than the current alternative
/* if (subtype.nameResId == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT
&& mainLayoutName()?.endsWith("+") != true // "+" layouts and custom layouts are always "unknown"
&& !LayoutUtilsCustom.isCustomLayout(mainLayoutName() ?: SubtypeLocaleUtils.QWERTY)
) {
// Skip unknown keyboard layout subtype. This may happen when predefined keyboard
// layout has been removed.
Log.w(SettingsSubtype::class.simpleName, "unknown additional subtype $this")
return null
}*/
return subtype
} }
fun mainLayoutName() = LayoutType.getMainLayoutFromExtraValue(extraValues) fun mainLayoutName() = LayoutType.getMainLayoutFromExtraValue(extraValues)
@ -54,7 +38,7 @@ data class SettingsSubtype(val locale: Locale, val extraValues: String) {
val newList = extraValues.split(",") val newList = extraValues.split(",")
.filterNot { it.isBlank() || it.startsWith("$extraValueKey=") || it == extraValueKey } .filterNot { it.isBlank() || it.startsWith("$extraValueKey=") || it == extraValueKey }
val newValue = if (extraValue == null) extraValueKey else "$extraValueKey=$extraValue" val newValue = if (extraValue == null) extraValueKey else "$extraValueKey=$extraValue"
val newValues = (newList + newValue).joinToString(",") val newValues = (newList + newValue).sorted().joinToString(",")
return copy(extraValues = newValues) return copy(extraValues = newValues)
} }

View file

@ -13,7 +13,6 @@ import android.text.TextUtils;
import helium314.keyboard.latin.App; import helium314.keyboard.latin.App;
import helium314.keyboard.latin.BuildConfig; import helium314.keyboard.latin.BuildConfig;
import helium314.keyboard.latin.settings.Defaults;
import helium314.keyboard.latin.settings.Settings; import helium314.keyboard.latin.settings.Settings;
import java.io.File; import java.io.File;
@ -63,7 +62,7 @@ public final class JniUtils {
// we want the default preferences, because storing the checksum in device protected storage is discouraged // we want the default preferences, because storing the checksum in device protected storage is discouraged
// see https://developer.android.com/reference/android/content/Context#createDeviceProtectedStorageContext() // see https://developer.android.com/reference/android/content/Context#createDeviceProtectedStorageContext()
// if device is locked, this will throw an IllegalStateException // if device is locked, this will throw an IllegalStateException
wantedChecksum = KtxKt.protectedPrefs(app).getString(Settings.PREF_LIBRARY_CHECKSUM, Defaults.PREF_LIBRARY_CHECKSUM); wantedChecksum = KtxKt.protectedPrefs(app).getString(Settings.PREF_LIBRARY_CHECKSUM, expectedDefaultChecksum());
} }
final FileInputStream libStream = new FileInputStream(userSuppliedLibrary); final FileInputStream libStream = new FileInputStream(userSuppliedLibrary);
final String checksum = ChecksumCalculator.INSTANCE.checksum(libStream); final String checksum = ChecksumCalculator.INSTANCE.checksum(libStream);

View file

@ -1,6 +1,8 @@
package helium314.keyboard.latin.utils package helium314.keyboard.latin.utils
import android.content.Context import android.content.Context
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.SimplePopups
import helium314.keyboard.keyboard.internal.keyboard_parser.getOrCreate
import helium314.keyboard.latin.R import helium314.keyboard.latin.R
import helium314.keyboard.latin.settings.Defaults.default import helium314.keyboard.latin.settings.Defaults.default
import helium314.keyboard.latin.utils.LayoutType.Companion.folder import helium314.keyboard.latin.utils.LayoutType.Companion.folder
@ -26,6 +28,7 @@ object LayoutUtils {
fun getLMainLayoutsForLocales(locales: List<Locale>, context: Context): Collection<String> = fun getLMainLayoutsForLocales(locales: List<Locale>, context: Context): Collection<String> =
locales.flatMapTo(HashSet()) { getAvailableLayouts(LayoutType.MAIN, context, it) }.sorted() locales.flatMapTo(HashSet()) { getAvailableLayouts(LayoutType.MAIN, context, it) }.sorted()
/** gets content for built-in (non-custom) layout [layoutName], with fallback to qwerty */
fun getContent(layoutType: LayoutType, layoutName: String, context: Context): String { fun getContent(layoutType: LayoutType, layoutName: String, context: Context): String {
val layouts = context.assets.list(layoutType.folder)!! val layouts = context.assets.list(layoutType.folder)!!
layouts.firstOrNull { it.startsWith("$layoutName.") } layouts.firstOrNull { it.startsWith("$layoutName.") }
@ -33,4 +36,27 @@ object LayoutUtils {
val fallback = layouts.first { it.startsWith(layoutType.default) } // must exist! val fallback = layouts.first { it.startsWith(layoutType.default) } // must exist!
return context.assets.open(layoutType.folder + File.separator + fallback).reader().readText() return context.assets.open(layoutType.folder + File.separator + fallback).reader().readText()
} }
fun getContentWithPlus(mainLayoutName: String, locale: Locale, context: Context): String {
val content = getContent(LayoutType.MAIN, mainLayoutName, context)
if (!mainLayoutName.endsWith("+"))
return content
// the stuff below will not work if we add "+" layouts in json format
// ideally we should serialize keyData to json to solve this
val rows = getSimpleRowStrings(content)
val localeKeyboardInfos = getOrCreate(context, locale)
return rows.mapIndexed { i, row ->
val extraKeys = localeKeyboardInfos.getExtraKeys(i + 1) ?: return@mapIndexed row
val rowList = row.split("\n").filterNot { it.isEmpty() }.toMutableList()
extraKeys.forEach { key ->
val popups = (key.popup as? SimplePopups)?.popupKeys?.joinToString(" ")
?.takeIf { it.isNotEmpty() }?.let { " $it" } ?: ""
rowList.add(key.label + popups)
}
rowList.joinToString("\n")
}.joinToString("\n\n")
}
fun getSimpleRowStrings(layoutContent: String): List<String> =
layoutContent.replace("\r\n", "\n").split("\\n\\s*\\n".toRegex()).filter { it.isNotBlank() }
} }

View file

@ -52,9 +52,8 @@ object SubtypeSettings {
fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) { fun addEnabledSubtype(prefs: SharedPreferences, newSubtype: InputMethodSubtype) {
val subtype = newSubtype.toSettingsSubtype() val subtype = newSubtype.toSettingsSubtype()
val subtypes = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!! val subtypes = createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!) + subtype
.split(Separators.SETS).filter { it.isNotBlank() }.map { it.toSettingsSubtype() } + subtype val newString = createPrefSubtypes(subtypes)
val newString = subtypes.map { it.toPref() }.toSortedSet().joinToString(Separators.SETS)
prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newString) } prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newString) }
if (newSubtype !in enabledSubtypes) { if (newSubtype !in enabledSubtypes) {
@ -74,10 +73,8 @@ object SubtypeSettings {
fun getSelectedSubtype(prefs: SharedPreferences): InputMethodSubtype { fun getSelectedSubtype(prefs: SharedPreferences): InputMethodSubtype {
val selectedSubtype = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype() val selectedSubtype = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype()
if (selectedSubtype.isAdditionalSubtype(prefs)) { if (selectedSubtype.isAdditionalSubtype(prefs))
val selectedAdditionalSubtype = selectedSubtype.toAdditionalSubtype() return selectedSubtype.toAdditionalSubtype()
if (selectedAdditionalSubtype != null) return selectedAdditionalSubtype
}
// no additional subtype, must be a resource subtype // no additional subtype, must be a resource subtype
val subtype = enabledSubtypes.firstOrNull { it.toSettingsSubtype() == selectedSubtype } val subtype = enabledSubtypes.firstOrNull { it.toSettingsSubtype() == selectedSubtype }
@ -157,6 +154,15 @@ object SubtypeSettings {
RichInputMethodManager.getInstance().refreshSubtypeCaches() RichInputMethodManager.getInstance().refreshSubtypeCaches()
} }
fun createSettingsSubtypes(prefSubtypes: String): List<SettingsSubtype> =
prefSubtypes.split(Separators.SETS).mapNotNull {
if (it.isEmpty()) null
else it.toSettingsSubtype()
}
fun createPrefSubtypes(subtypes: Collection<SettingsSubtype>): String =
subtypes.map { it.toPref() }.toSortedSet().joinToString(Separators.SETS)
fun init(context: Context) { fun init(context: Context) {
SubtypeLocaleUtils.init(context) // necessary to get the correct getKeyboardLayoutSetName SubtypeLocaleUtils.init(context) // necessary to get the correct getKeyboardLayoutSetName
@ -207,7 +213,8 @@ object SubtypeSettings {
} }
if (subtypesToRemove.isEmpty()) return if (subtypesToRemove.isEmpty()) return
Log.w(TAG, "removing custom subtypes without main layout files: $subtypesToRemove") Log.w(TAG, "removing custom subtypes without main layout files: $subtypesToRemove")
Settings.writePrefAdditionalSubtypes(prefs, additionalSubtypes.filterNot { it in subtypesToRemove }.joinToString(Separators.SETS)) // todo: now we have a qwerty fallback anyway, consider removing this method (makes bugs more obvious to users)
prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, additionalSubtypes.filterNot { it in subtypesToRemove }.joinToString(Separators.SETS)).apply()
} }
private fun loadAdditionalSubtypes(prefs: SharedPreferences) { private fun loadAdditionalSubtypes(prefs: SharedPreferences) {
@ -220,16 +227,12 @@ object SubtypeSettings {
// 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 settingsSubtypes = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!! val settingsSubtypes = createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!)
.split(Separators.SETS).filter { it.isNotEmpty() }.map { it.toSettingsSubtype() }
for (settingsSubtype in settingsSubtypes) { for (settingsSubtype in settingsSubtypes) {
if (settingsSubtype.isAdditionalSubtype(prefs)) { if (settingsSubtype.isAdditionalSubtype(prefs)) {
val additionalSubtype = settingsSubtype.toAdditionalSubtype() enabledSubtypes.add(settingsSubtype.toAdditionalSubtype())
if (additionalSubtype != null) {
enabledSubtypes.add(additionalSubtype)
continue continue
} }
}
val subtypesForLocale = resourceSubtypesByLocale[settingsSubtype.locale] val subtypesForLocale = resourceSubtypesByLocale[settingsSubtype.locale]
if (subtypesForLocale == null) { if (subtypesForLocale == null) {
val message = "no resource subtype for $settingsSubtype" val message = "no resource subtype for $settingsSubtype"
@ -258,12 +261,11 @@ object SubtypeSettings {
/** @return whether pref was changed */ /** @return whether pref was changed */
private fun removeEnabledSubtype(prefs: SharedPreferences, subtype: SettingsSubtype): Boolean { private fun removeEnabledSubtype(prefs: SharedPreferences, subtype: SettingsSubtype): Boolean {
val oldSubtypes = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!! val oldSubtypes = createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!)
.split(Separators.SETS).filter { it.isNotEmpty() }.map { it.toSettingsSubtype() }
val newSubtypes = oldSubtypes - subtype val newSubtypes = oldSubtypes - subtype
if (oldSubtypes == newSubtypes) if (oldSubtypes == newSubtypes)
return false // already removed return false // already removed
prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, newSubtypes.joinToString(Separators.SETS) { it.toPref() }) } prefs.edit { putString(Settings.PREF_ENABLED_SUBTYPES, createPrefSubtypes(newSubtypes)) }
if (subtype == prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype()) { if (subtype == prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype()) {
// switch subtype if the currently used one has been disabled // switch subtype if the currently used one has been disabled
try { try {

View file

@ -53,10 +53,11 @@ object SubtypeUtilsAdditional {
val prefs = context.prefs() val prefs = context.prefs()
SubtypeSettings.removeEnabledSubtype(context, subtype) SubtypeSettings.removeEnabledSubtype(context, subtype)
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 = SubtypeSettings.createSettingsSubtypes(oldAdditionalSubtypesString)
val newAdditionalSubtypes = oldAdditionalSubtypes.filter { it != subtype } val settingsSubtype = subtype.toSettingsSubtype()
val newAdditionalSubtypesString = createPrefSubtypes(newAdditionalSubtypes) val newAdditionalSubtypes = oldAdditionalSubtypes.filter { it != settingsSubtype }
Settings.writePrefAdditionalSubtypes(prefs, newAdditionalSubtypesString) val newAdditionalSubtypesString = SubtypeSettings.createPrefSubtypes(newAdditionalSubtypes)
prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, newAdditionalSubtypesString).apply()
} }
// updates additional subtypes, enabled subtypes, and selected subtype // updates additional subtypes, enabled subtypes, and selected subtype
@ -66,33 +67,36 @@ object SubtypeUtilsAdditional {
val isSelected = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype() == from val isSelected = prefs.getString(Settings.PREF_SELECTED_SUBTYPE, Defaults.PREF_SELECTED_SUBTYPE)!!.toSettingsSubtype() == from
val isEnabled = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!.split(Separators.SETS) val isEnabled = prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!.split(Separators.SETS)
.any { it.toSettingsSubtype() == from } .any { it.toSettingsSubtype() == from }
val new = prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!! val additionalSubtypes = SubtypeSettings.createSettingsSubtypes(prefs.getString(Settings.PREF_ADDITIONAL_SUBTYPES, Defaults.PREF_ADDITIONAL_SUBTYPES)!!)
.split(Separators.SETS).mapNotNullTo(sortedSetOf()) { .toMutableList()
if (it == from.toPref()) null else it additionalSubtypes.remove(from)
} + to.toPref() if (SubtypeSettings.getResourceSubtypesForLocale(to.locale).none { it.toSettingsSubtype() == to }) {
prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, new.joinToString(Separators.SETS)).apply() // We only add the "to" subtype if it's not equal to a resource subtype.
// This means we make additional subtype disappear as magically as it was added if all settings are default.
val fromSubtype = from.toAdditionalSubtype() // will be null if we edit a resource subtype // If we don't do this, enabling the base subtype will result in the additional subtype being enabled,
val toSubtype = to.toAdditionalSubtype() // should never be null // as both have the same settingsSubtype.
if (isSelected && toSubtype != null) { additionalSubtypes.add(to)
SubtypeSettings.setSelectedSubtype(prefs, toSubtype)
} }
if (fromSubtype != null && isEnabled && toSubtype != null) { val editor = prefs.edit()
SubtypeSettings.removeEnabledSubtype(context, fromSubtype) editor.putString(Settings.PREF_ADDITIONAL_SUBTYPES, SubtypeSettings.createPrefSubtypes(additionalSubtypes))
SubtypeSettings.addEnabledSubtype(prefs, toSubtype) if (isSelected) {
editor.putString(Settings.PREF_SELECTED_SUBTYPE, to.toPref())
} }
if (isEnabled) {
val enabled = SubtypeSettings.createSettingsSubtypes(prefs.getString(Settings.PREF_ENABLED_SUBTYPES, Defaults.PREF_ENABLED_SUBTYPES)!!)
.toMutableList()
enabled.remove(from)
enabled.add(to)
editor.putString(Settings.PREF_ENABLED_SUBTYPES, SubtypeSettings.createPrefSubtypes(enabled))
}
editor.apply()
SubtypeSettings.reloadEnabledSubtypes(context)
} }
fun createAdditionalSubtypes(prefSubtypes: String): List<InputMethodSubtype> { fun createAdditionalSubtypes(prefSubtypes: String): List<InputMethodSubtype> =
if (prefSubtypes.isEmpty()) prefSubtypes.split(Separators.SETS).mapNotNull {
return emptyList() if (it.isEmpty()) null
return prefSubtypes.split(Separators.SETS).mapNotNull { it.toSettingsSubtype().toAdditionalSubtype() } else it.toSettingsSubtype().toAdditionalSubtype()
}
fun createPrefSubtypes(subtypes: Collection<InputMethodSubtype>): String {
if (subtypes.isEmpty())
return ""
return subtypes.joinToString(Separators.SETS) { it.toSettingsSubtype().toPref() }
} }
private fun getNameResId(locale: Locale, mainLayoutName: String): Int { private fun getNameResId(locale: Locale, mainLayoutName: String): Int {

View file

@ -125,8 +125,8 @@ fun SubtypeDialog(
onConfirmed = { onConfirmed(currentSubtype) }, onConfirmed = { onConfirmed(currentSubtype) },
neutralButtonText = if (initialSubtype.isAdditionalSubtype(prefs)) stringResource(R.string.delete) else null, neutralButtonText = if (initialSubtype.isAdditionalSubtype(prefs)) stringResource(R.string.delete) else null,
onNeutral = { onNeutral = {
SubtypeUtilsAdditional.removeAdditionalSubtype(ctx, initialSubtype.toAdditionalSubtype()!!) SubtypeUtilsAdditional.removeAdditionalSubtype(ctx, initialSubtype.toAdditionalSubtype())
SubtypeSettings.removeEnabledSubtype(ctx, initialSubtype.toAdditionalSubtype()!!) SubtypeSettings.removeEnabledSubtype(ctx, initialSubtype.toAdditionalSubtype())
onDismissRequest() onDismissRequest()
}, },
title = { title = {
@ -393,7 +393,7 @@ private fun MainLayoutRow(
if (showLayoutEditDialog != null) { if (showLayoutEditDialog != null) {
val layoutName = showLayoutEditDialog!!.first val layoutName = showLayoutEditDialog!!.first
val startContent = showLayoutEditDialog?.second val startContent = showLayoutEditDialog?.second
?: if (layoutName in appLayouts) LayoutUtils.getContent(LayoutType.MAIN, layoutName, ctx) ?: if (layoutName in appLayouts) LayoutUtils.getContentWithPlus(layoutName, currentSubtype.locale, ctx)
else null else null
LayoutEditDialog( LayoutEditDialog(
onDismissRequest = { showLayoutEditDialog = null }, onDismissRequest = { showLayoutEditDialog = null },

View file

@ -35,10 +35,13 @@ fun LoadGestureLibPreference(setting: Setting) {
val abi = Build.SUPPORTED_ABIS[0] val abi = Build.SUPPORTED_ABIS[0]
val libFile = File(ctx.filesDir?.absolutePath + File.separator + JniUtils.JNI_LIB_IMPORT_FILE_NAME) val libFile = File(ctx.filesDir?.absolutePath + File.separator + JniUtils.JNI_LIB_IMPORT_FILE_NAME)
fun renameToLibFileAndRestart(file: File, checksum: String) { fun renameToLibFileAndRestart(file: File, checksum: String) {
libFile.setWritable(true)
libFile.delete() libFile.delete()
// store checksum in default preferences (soo JniUtils) // store checksum in default preferences (see JniUtils)
prefs.edit().putString(Settings.PREF_LIBRARY_CHECKSUM, checksum).commit() prefs.edit().putString(Settings.PREF_LIBRARY_CHECKSUM, checksum).commit()
file.renameTo(libFile) file.copyTo(libFile)
libFile.setReadOnly()
file.delete()
Runtime.getRuntime().exit(0) // exit will restart the app, so library will be loaded Runtime.getRuntime().exit(0) // exit will restart the app, so library will be loaded
} }
var tempFilePath: String? by rememberSaveable { mutableStateOf(null) } var tempFilePath: String? by rememberSaveable { mutableStateOf(null) }

View file

@ -0,0 +1,75 @@
package helium314.keyboard
import helium314.keyboard.keyboard.KeyboardId
import helium314.keyboard.keyboard.KeyboardLayoutSet
import helium314.keyboard.keyboard.internal.KeyboardParams
import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL
import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
import helium314.keyboard.latin.LatinIME
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.settings.SettingsSubtype.Companion.toSettingsSubtype
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.POPUP_KEYS_LAYOUT
import helium314.keyboard.latin.utils.SubtypeSettings
import helium314.keyboard.latin.utils.SubtypeUtilsAdditional
import helium314.keyboard.latin.utils.prefs
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowLog
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@RunWith(RobolectricTestRunner::class)
@Config(shadows = [
ShadowInputMethodManager2::class
])
class SubtypeTest {
private lateinit var latinIME: LatinIME
private lateinit var params: KeyboardParams
@BeforeTest fun setUp() {
latinIME = Robolectric.setupService(LatinIME::class.java)
ShadowLog.setupLogging()
ShadowLog.stream = System.out
params = KeyboardParams()
params.mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET)
params.mPopupKeyTypes.add(POPUP_KEYS_LAYOUT)
addLocaleKeyTextsToParams(latinIME, params, POPUP_KEYS_NORMAL)
}
@Test fun emptyAdditionalSubtypesResultsInEmptyList() {
// avoid issues where empty string results in additional subtype for undefined locale
val prefs = latinIME.prefs()
prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, "").apply()
assertTrue(SubtypeSettings.getAdditionalSubtypes().isEmpty())
val from = SubtypeSettings.getResourceSubtypesForLocale("es".constructLocale()).first()
// no change, and "changed" subtype actually is resource subtype -> still expect empty list
SubtypeUtilsAdditional.changeAdditionalSubtype(from.toSettingsSubtype(), from.toSettingsSubtype(), latinIME)
assertEquals(emptyList(), SubtypeSettings.getAdditionalSubtypes().map { it.toSettingsSubtype() })
}
@Test fun subtypeStaysEnabledOnEdits() {
val prefs = latinIME.prefs()
prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, "").apply() // clear it for convenience
// edit enabled resource subtype
val from = SubtypeSettings.getResourceSubtypesForLocale("es".constructLocale()).first()
SubtypeSettings.addEnabledSubtype(prefs, from)
val to = from.toSettingsSubtype().withLayout(LayoutType.SYMBOLS, "symbols_arabic")
SubtypeUtilsAdditional.changeAdditionalSubtype(from.toSettingsSubtype(), to, latinIME)
assertEquals(to, SubtypeSettings.getEnabledSubtypes(false).single().toSettingsSubtype())
// change the new subtype to effectively be the same as original resource subtype
val toNew = to.withoutLayout(LayoutType.SYMBOLS)
assertEquals(from.toSettingsSubtype(), toNew)
SubtypeUtilsAdditional.changeAdditionalSubtype(to, toNew, latinIME)
assertEquals(emptyList(), SubtypeSettings.getAdditionalSubtypes().map { it.toSettingsSubtype() })
assertEquals(from.toSettingsSubtype(), SubtypeSettings.getEnabledSubtypes(false).single().toSettingsSubtype())
}
}