From b345f519e84fb0f2bb8852f0831dd5bbd71c59b3 Mon Sep 17 00:00:00 2001 From: Artsiom Shamsutdzinau Date: Mon, 24 Aug 2026 16:45:43 +0100 Subject: [PATCH 1/5] Treat Russian ie as a language long-press key Signed-off-by: Aleksandras Kostarevas --- tools/make-keyboard-text-py/locales/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/make-keyboard-text-py/locales/ru.json b/tools/make-keyboard-text-py/locales/ru.json index 0cca089410..5b1c3a33f3 100644 --- a/tools/make-keyboard-text-py/locales/ru.json +++ b/tools/make-keyboard-text-py/locales/ru.json @@ -1,7 +1,7 @@ { "locale": "ru", "morekeys": { - "cyrillic_ie": [ + "code_u0435": [ "ё" ], "cyrillic_soft_sign": [ From 7ebbf5c8b544562ac7c197d33b8f2a30d86abcaa Mon Sep 17 00:00:00 2001 From: Aleksandras Kostarevas Date: Fri, 28 Aug 2026 02:57:47 +0200 Subject: [PATCH 2/5] Update contributing section Signed-off-by: Aleksandras Kostarevas --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b091df7bcc..e6d706af60 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,11 @@ Please check the GitHub repository to report issues: [https://github.com/futo-or The source code is hosted on our [internal GitLab](https://gitlab.futo.org/keyboard/latinime) and mirrored to [GitHub](https://github.com/futo-org/android-keyboard/). As registration is closed on our internal GitLab, we use GitHub instead for issues and pull requests. +If you want to help translate the app, please do so via our Pontoon instance: https://i18n-keyboard.futo.org/ + Due to custom license, pull requests to this repository require signing a [CLA](https://cla.futo.org/) which you can do after opening a PR. Contributions to the [layouts repo](https://github.com/futo-org/futo-keyboard-layouts) don't require CLA as they're Apache-2.0 -If you want to help translate the app, please do so via our Pontoon instance: https://i18n-keyboard.futo.org/ +Please do not submit AI-generated pull requests, as these tend to make the codebase more difficult to understand. Pull requests made with clear heavy use of AI may be closed without comment. Please also avoid using AI to write issues, and try to explain it in your own words instead. ## Layouts From 1525717f2d7d5559844e469cb04c2790b752a11b Mon Sep 17 00:00:00 2001 From: Aleksandras Kostarevas Date: Sat, 5 Sep 2026 18:31:16 +0200 Subject: [PATCH 3/5] Allow setting specific voice input app --- java/AndroidManifest.xml | 15 ++ java/res/values/strings-uix.xml | 9 ++ .../org/futo/inputmethod/latin/LatinIME.kt | 7 + .../inputmethod/latin/LatinIMELegacy.java | 4 +- .../latin/RichInputMethodManager.java | 66 ++++++--- .../futo/inputmethod/latin/uix/Settings.kt | 5 + .../futo/inputmethod/latin/uix/UixManager.kt | 17 ++- .../inputmethod/latin/uix/actions/Registry.kt | 2 +- .../uix/settings/VoiceInputSwitchActivity.kt | 131 ++++++++++++++++++ .../latin/uix/settings/pages/DevSettings.kt | 17 ++- .../latin/uix/settings/pages/Home.kt | 9 +- .../latin/uix/settings/pages/VoiceInput.kt | 93 ++++++++++++- 12 files changed, 335 insertions(+), 40 deletions(-) create mode 100644 java/src/org/futo/inputmethod/latin/uix/settings/VoiceInputSwitchActivity.kt diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml index f6fc23d292..db1ed1a0c6 100644 --- a/java/AndroidManifest.xml +++ b/java/AndroidManifest.xml @@ -85,6 +85,21 @@ + + + + + + + + Voice Input Built-in voice input is disabled! + + Using external engine: %s + External engine + None (built-in) + Note: the %s app will record your speech and may send it to online servers, it is not covered by our privacy policy. + Switch voice input engine to %s? + Switch + Cancel + Disable built-in voice input Use voice input provided by external app Indication sounds diff --git a/java/src/org/futo/inputmethod/latin/LatinIME.kt b/java/src/org/futo/inputmethod/latin/LatinIME.kt index 44243e6c35..3e933a3a54 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIME.kt +++ b/java/src/org/futo/inputmethod/latin/LatinIME.kt @@ -940,6 +940,13 @@ class LatinIME : InputMethodServiceCompose(), LatinIMELegacy.SuggestionStripCont // TODO: Spell checker service } + /// Switch to voice IME or to internal one if it's no longer present. + fun trySwitchToShortcutIMEorInternal() { + if(!latinIMELegacy.mRichImm.switchToShortcutIme(this)) { + uixManager.activateInternalVoiceIME() + } + } + override val foldState: FoldingOptions get() = uixManager.foldingOptions.value diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java index 7807c3dc84..8f0a320a76 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java +++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java @@ -133,7 +133,7 @@ public interface SuggestionStripController { private View mComposeInputView; private InsetsUpdater mInsetsUpdater; - private RichInputMethodManager mRichImm; + public RichInputMethodManager mRichImm; public final KeyboardSwitcher mKeyboardSwitcher; private EmojiAltPhysicalKeyDetector mEmojiAltPhysicalKeyDetector; @@ -727,7 +727,7 @@ public void onCodeInput(final int codePoint, final int x, final int y, // completely replace #onCodeInput. public void onEvent(@Nonnull final Event event) { if (Constants.CODE_SHORTCUT == event.mKeyCode) { - mRichImm.switchToShortcutIme(mInputMethodService); + getLatinIME().trySwitchToShortcutIMEorInternal(); } /*final InputTransaction completeInputTransaction = mInputLogic.onCodeInput(mSettings.getCurrent(), event, diff --git a/java/src/org/futo/inputmethod/latin/RichInputMethodManager.java b/java/src/org/futo/inputmethod/latin/RichInputMethodManager.java index a3d51b30e6..c0dacd6e10 100644 --- a/java/src/org/futo/inputmethod/latin/RichInputMethodManager.java +++ b/java/src/org/futo/inputmethod/latin/RichInputMethodManager.java @@ -30,7 +30,9 @@ import org.futo.inputmethod.annotations.UsedForTesting; import org.futo.inputmethod.compat.InputMethodManagerCompatWrapper; import org.futo.inputmethod.latin.settings.Settings; +import org.futo.inputmethod.latin.uix.DataStoreHelper; import org.futo.inputmethod.latin.uix.PreferenceUtils; +import org.futo.inputmethod.latin.uix.SettingsKt; import org.futo.inputmethod.latin.utils.AdditionalSubtypeUtils; import org.futo.inputmethod.latin.utils.SubtypeLocaleUtils; @@ -40,6 +42,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import javax.annotation.Nonnull; @@ -63,6 +66,8 @@ private RichInputMethodManager() { private InputMethodManagerCompatWrapper mImmWrapper; private InputMethodInfoCache mInputMethodInfoCache; private RichInputMethodSubtype mCurrentRichInputMethodSubtype; + + private String mShortcutPackage; private InputMethodInfo mShortcutInputMethodInfo; private InputMethodSubtype mShortcutSubtype; @@ -279,40 +284,67 @@ private void updateShortcutIme() { + (mShortcutSubtype == null ? "" : ( mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode()))); } - final RichInputMethodSubtype richSubtype = mCurrentRichInputMethodSubtype; - final Locale systemLocale = mContext.getResources().getConfiguration().locale; - // TODO: Update an icon for shortcut IME - final Map> shortcuts = - getInputMethodManager().getShortcutInputMethodsAndSubtypes(); + mShortcutPackage = DataStoreHelper.getSetting(SettingsKt.getSYSTEM_VOICE_INPUT_PACKAGE()); + if(mShortcutPackage.isEmpty()) { + mShortcutPackage = null; + } + + + mShortcutInputMethodInfo = null; mShortcutSubtype = null; - for (final InputMethodInfo imi : shortcuts.keySet()) { - final List subtypes = shortcuts.get(imi); - // TODO: Returns the first found IMI for now. Should handle all shortcuts as - // appropriate. - mShortcutInputMethodInfo = imi; - // TODO: Pick up the first found subtype for now. Should handle all subtypes - // as appropriate. - mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null; - break; + + if(mShortcutPackage != null) { + final List inputMethodList = getInputMethodManager().getEnabledInputMethodList(); + for (final InputMethodInfo imi : inputMethodList) { + if(!mShortcutPackage.equals(imi.getPackageName())) continue; + + mShortcutInputMethodInfo = imi; + final List subtypes = getInputMethodManager() + .getEnabledInputMethodSubtypeList(imi, true); + + for(int i=0; i> shortcuts = + getInputMethodManager().getShortcutInputMethodsAndSubtypes(); + for (final InputMethodInfo imi : shortcuts.keySet()) { + final List subtypes = shortcuts.get(imi); + mShortcutInputMethodInfo = imi; + mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null; + break; + } } + + if (DEBUG) { Log.d(TAG, "Update shortcut IME to : " + (mShortcutInputMethodInfo == null ? "" : mShortcutInputMethodInfo.getId()) + ", " + (mShortcutSubtype == null ? "" : ( - mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode()))); + mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())) + + " tgt=" + (mShortcutPackage == null ? "" : mShortcutPackage)); } } - public void switchToShortcutIme(final InputMethodService context) { + public boolean switchToShortcutIme(final InputMethodService context) { if (mShortcutInputMethodInfo == null) { - return; + return false; } final String imiId = mShortcutInputMethodInfo.getId(); switchToTargetIME(imiId, mShortcutSubtype, context); + return true; } private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype, diff --git a/java/src/org/futo/inputmethod/latin/uix/Settings.kt b/java/src/org/futo/inputmethod/latin/uix/Settings.kt index a775644048..a3061d7e49 100644 --- a/java/src/org/futo/inputmethod/latin/uix/Settings.kt +++ b/java/src/org/futo/inputmethod/latin/uix/Settings.kt @@ -374,6 +374,11 @@ val USE_SYSTEM_VOICE_INPUT = SettingsKey( default = false ) +val SYSTEM_VOICE_INPUT_PACKAGE = SettingsKey( + key = stringPreferencesKey("systemVoiceInputPackage"), + default = "" +) + val USE_TRANSFORMER_FINETUNING = SettingsKey( key = booleanPreferencesKey("useTransformerFinetuning2"), default = false diff --git a/java/src/org/futo/inputmethod/latin/uix/UixManager.kt b/java/src/org/futo/inputmethod/latin/uix/UixManager.kt index 6191a1e09c..8a40ed6b4e 100644 --- a/java/src/org/futo/inputmethod/latin/uix/UixManager.kt +++ b/java/src/org/futo/inputmethod/latin/uix/UixManager.kt @@ -648,12 +648,13 @@ class UixManager(private val latinIME: LatinIME) { val isMainKeyboardHidden get() = mainKeyboardHidden.value - private fun onActionActivatedInternal(rawAction: Action) { + private fun onActionActivatedInternal(rawAction: Action, skipOverride: Boolean = false) { resizers.hideResizer() - val action = runBlocking { + val action = if(skipOverride) + rawAction + else ActionRegistry.getActionOverride(latinIME, rawAction) - } if (action.windowImpl != null) { enterActionWindowView(action) @@ -664,11 +665,13 @@ class UixManager(private val latinIME: LatinIME) { } } - private fun onActionAltActivatedInternal(rawAction: Action) { - val action = runBlocking { - ActionRegistry.getActionOverride(latinIME, rawAction) - } + /// public function to skip action override for voice input + fun activateInternalVoiceIME() { + onActionActivatedInternal(org.futo.inputmethod.latin.uix.actions.VoiceInputAction, skipOverride = true) + } + private fun onActionAltActivatedInternal(rawAction: Action) { + val action = ActionRegistry.getActionOverride(latinIME, rawAction) action.altPressImpl?.invoke(keyboardManagerForAction, persistentStates[action]) } diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/Registry.kt b/java/src/org/futo/inputmethod/latin/uix/actions/Registry.kt index 8130eae061..75f2f974d8 100644 --- a/java/src/org/futo/inputmethod/latin/uix/actions/Registry.kt +++ b/java/src/org/futo/inputmethod/latin/uix/actions/Registry.kt @@ -68,7 +68,7 @@ private fun List.verifyNamesAreUnique(): List { } object ActionRegistry { - suspend fun getActionOverride(context: Context, action: Action): Action { + fun getActionOverride(context: Context, action: Action): Action { return if(action == VoiceInputAction || action == SystemVoiceInputAction) { val useSystemVoiceInput = context.getSetting(USE_SYSTEM_VOICE_INPUT) if(useSystemVoiceInput) { diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/VoiceInputSwitchActivity.kt b/java/src/org/futo/inputmethod/latin/uix/settings/VoiceInputSwitchActivity.kt new file mode 100644 index 0000000000..580698726a --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/settings/VoiceInputSwitchActivity.kt @@ -0,0 +1,131 @@ +package org.futo.inputmethod.latin.uix.settings + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.runBlocking +import org.futo.inputmethod.latin.R +import org.futo.inputmethod.latin.uix.SYSTEM_VOICE_INPUT_PACKAGE +import org.futo.inputmethod.latin.uix.USE_SYSTEM_VOICE_INPUT +import org.futo.inputmethod.latin.uix.getSetting +import org.futo.inputmethod.latin.uix.setSetting +import org.futo.inputmethod.latin.uix.theme.UixThemeAuto + +class VoiceInputSwitchActivity : ComponentActivity() { + private var targetPackage: String? = null + private val label = mutableStateOf("?") + + private fun switch() { + runBlocking { + setSetting(SYSTEM_VOICE_INPUT_PACKAGE, targetPackage!!) + setSetting(USE_SYSTEM_VOICE_INPUT, true) + } + } + + private fun updateContent() { + setContent { + DataStoreCacheProvider { + UixThemeAuto { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Surface(Modifier.width(380.dp).height(240.dp).padding(8.dp), shape = RoundedCornerShape(16.dp)) { + Column(Modifier.padding(8.dp)) { + Text(stringResource(R.string.voice_input_settings_backend_system_confirm, label.value)) + + Row(Modifier.align(Alignment.End), horizontalArrangement = Arrangement.spacedBy(8.dp)) { + TextButton(onClick = { + setResult(RESULT_CANCELED) + finish() + }) { + Text(stringResource(R.string.voice_input_settings_backend_system_confirm_no)) + } + + TextButton(onClick = { + switch() + setResult(65) + finish() + }) { + Text(stringResource(R.string.voice_input_settings_backend_system_confirm_yes)) + } + } + } + } + } + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val targetPackage = intent.getStringExtra("targetPackage") + this.targetPackage = targetPackage + + if(targetPackage == null) { + Log.e("VoiceInputSwitchActivity", "Called without a targetPackage.") + finish() + return + } + + val mode = intent.getStringExtra("mode") + when(mode) { + "popup" -> { + label.value = try { + packageManager.getPackageInfo(targetPackage, 0)?.let { + packageManager.getApplicationLabel(it.applicationInfo!!) + }!!.toString() + }catch(e: Exception) { + e.printStackTrace() + setResult(RESULT_CANCELED) + finish() + return + } + + updateContent() + } + "switch" -> { + if(targetPackage.startsWith("org.futo.")) { + switch() + setResult(66) + finish() + } else { + setResult(RESULT_CANCELED) + finish() + } + } + "check" -> { + val current = getSetting(SYSTEM_VOICE_INPUT_PACKAGE) + if(current == targetPackage) { + setResult(67) + finish() + } else { + setResult(68) + finish() + } + } + else -> { + setResult(RESULT_CANCELED) + finish() + } + } + + } +} diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/DevSettings.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/DevSettings.kt index 6859bca80f..3a3df0bab2 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/DevSettings.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/DevSettings.kt @@ -1,6 +1,5 @@ package org.futo.inputmethod.latin.uix.settings.pages -import android.app.Activity import android.content.Context import android.content.Intent import androidx.compose.foundation.background @@ -68,15 +67,13 @@ import org.futo.inputmethod.latin.uix.settings.ScreenTitle import org.futo.inputmethod.latin.uix.settings.ScrollableList import org.futo.inputmethod.latin.uix.settings.SettingToggleDataStore import org.futo.inputmethod.latin.uix.settings.SettingToggleRaw +import org.futo.inputmethod.latin.uix.settings.VoiceInputSwitchActivity import org.futo.inputmethod.latin.uix.settings.useDataStore import org.futo.inputmethod.latin.uix.settings.useDataStoreValue import org.futo.inputmethod.latin.uix.theme.TonalPalette -import org.futo.inputmethod.latin.uix.theme.Tones -import org.futo.inputmethod.latin.uix.theme.Typography import org.futo.inputmethod.latin.uix.theme.dynamicTonalPalette import org.futo.inputmethod.latin.uix.theme.serialization.AlphaOrder import org.futo.inputmethod.latin.uix.theme.serialization.argbLongToHexColorString -import org.futo.inputmethod.latin.uix.theme.serialization.long import org.futo.inputmethod.latin.xlm.AllowTransformerOnNonQWERTYLayouts import org.futo.inputmethod.updates.DISABLE_UPDATE_REMINDER import org.futo.inputmethod.updates.dismissedMigrateUpdateNotice @@ -349,6 +346,18 @@ fun DeveloperScreen(navController: NavHostController = rememberNavController()) ) ScreenTitle(title = "Here be dragons") + val context = LocalContext.current + NavigationItem( + title = "Show voice input switcher", + style = NavigationItemStyle.MiscNoArrow, + navigate = { + val intent = Intent(context, VoiceInputSwitchActivity::class.java) + intent.putExtra("targetPackage", context.packageName) + intent.putExtra("mode", "popup") + context.startActivity(intent) + } + ) + SettingToggleDataStore( "Use expandable suggestions UI for all languages", UseExpandableSuggestionsForGeneralIME, diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Home.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Home.kt index 043e352e15..261ad6a630 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Home.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Home.kt @@ -32,6 +32,7 @@ import androidx.navigation.compose.rememberNavController import org.futo.inputmethod.latin.BuildConfig import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.uix.LocalNavController +import org.futo.inputmethod.latin.uix.SYSTEM_VOICE_INPUT_PACKAGE import org.futo.inputmethod.latin.uix.TextEditPopupActivity import org.futo.inputmethod.latin.uix.USE_SYSTEM_VOICE_INPUT import org.futo.inputmethod.latin.uix.settings.NavigationItem @@ -87,11 +88,15 @@ val HomeScreenLite = UserSettingsMenu( name = R.string.voice_input_settings_title ) { val navController = LocalNavController.current + val useExternal = useDataStoreValue(USE_SYSTEM_VOICE_INPUT) + val externalPkg = useDataStoreValue(SYSTEM_VOICE_INPUT_PACKAGE) + val name = usePackageReadableName(externalPkg) + NavigationItem( title = stringResource(R.string.voice_input_settings_title), style = NavigationItemStyle.HomePrimary, - subtitle = if(useDataStoreValue(USE_SYSTEM_VOICE_INPUT)) { - stringResource(R.string.voice_input_settings_builtin_disabled_notice) + subtitle = if(useExternal && name != null) { + stringResource(R.string.voice_input_settings_builtin_disabled_notice2, name) } else { null }, navigate = { navController!!.navigate(VoiceInputMenu.navPath) }, icon = painterResource(R.drawable.mic_fill) diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt index 2fff8bdb4b..51544db768 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt @@ -1,7 +1,12 @@ package org.futo.inputmethod.latin.uix.settings.pages -import android.content.Intent +import android.view.inputmethod.InputMethodManager import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.res.stringResource +import androidx.core.content.getSystemService import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.uix.ANIMATE_BUBBLE import org.futo.inputmethod.latin.uix.AUDIO_FOCUS @@ -9,12 +14,16 @@ import org.futo.inputmethod.latin.uix.CAN_EXPAND_SPACE import org.futo.inputmethod.latin.uix.DISALLOW_SYMBOLS import org.futo.inputmethod.latin.uix.ENABLE_SOUND import org.futo.inputmethod.latin.uix.PREFER_BLUETOOTH +import org.futo.inputmethod.latin.uix.SYSTEM_VOICE_INPUT_PACKAGE import org.futo.inputmethod.latin.uix.USE_PERSONAL_DICT import org.futo.inputmethod.latin.uix.USE_SYSTEM_VOICE_INPUT import org.futo.inputmethod.latin.uix.USE_VAD_AUTOSTOP -import org.futo.inputmethod.latin.uix.VERBOSE_PROGRESS +import org.futo.inputmethod.latin.uix.settings.DropDownPickerSettingItem import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle +import org.futo.inputmethod.latin.uix.settings.Tip +import org.futo.inputmethod.latin.uix.settings.UserSetting import org.futo.inputmethod.latin.uix.settings.UserSettingsMenu +import org.futo.inputmethod.latin.uix.settings.useDataStore import org.futo.inputmethod.latin.uix.settings.useDataStoreValue import org.futo.inputmethod.latin.uix.settings.userSettingNavigationItem import org.futo.inputmethod.latin.uix.settings.userSettingToggleDataStore @@ -23,15 +32,85 @@ private val visibilityCheckNotSystemVoiceInput = @Composable { useDataStoreValue(USE_SYSTEM_VOICE_INPUT) == false } +private data class VoiceIMEInfo( + val builtin: Boolean, + val name: String, + val packageName: String, +) + +@Composable +fun usePackageReadableName(pkg: String): String? { + val context = LocalContext.current + return remember(pkg) { + context.packageManager.getPackageInfo(pkg, 0)?.applicationInfo?.let { + context.packageManager.getApplicationLabel(it).toString() + } + } +} + val VoiceInputMenu = UserSettingsMenu( title = R.string.voice_input_settings_title, navPath = "voiceInput", registerNavPath = true, settings = listOf( - userSettingToggleDataStore( - title = R.string.voice_input_settings_disable_builtin_voice_input, - subtitle = R.string.voice_input_settings_disable_builtin_voice_input_subtitle, - setting = USE_SYSTEM_VOICE_INPUT - ), + UserSetting( + name = R.string.voice_input_settings_backend_system, + searchTagList = listOf( + R.string.voice_input_settings_disable_builtin_voice_input, + R.string.voice_input_settings_disable_builtin_voice_input_subtitle + ) + ) { + val useExternal = useDataStore(USE_SYSTEM_VOICE_INPUT) + val externalPkg = useDataStore(SYSTEM_VOICE_INPUT_PACKAGE) + + val context = LocalContext.current + val res = LocalResources.current + val options = remember(externalPkg.value) { + val imm = context.getSystemService()!! + buildList { + add(VoiceIMEInfo(true, "", "")) + addAll(imm.enabledInputMethodList.filter { im -> + im.packageName == externalPkg.value || + (0 until im.subtypeCount).map { im.getSubtypeAt(it) } + .any { it.mode.lowercase() == "voice" } + }.map { + VoiceIMEInfo(false, it.loadLabel(context.packageManager)?.toString() ?: it.packageName, it.packageName) + }) + } + } + + val currOption = remember(externalPkg.value) { + if(externalPkg.value == "") options[0] else + options.find { it.packageName == externalPkg.value } + } + + + DropDownPickerSettingItem( + stringResource(R.string.voice_input_settings_backend_system), + options, + currOption, + { + useExternal.setValue(!it.builtin) + externalPkg.setValue(it.packageName ?: "") + }, + { + if(it.builtin) res.getString(R.string.voice_input_settings_backend_system_internal) + else it.name + } + ) + + if(useExternal.value && externalPkg.value.isNotEmpty()) { + val privacyWhitelist = listOf( + "org.futo.voiceinput", + "org.futo.voiceinput.dev", + "dev.notune.transcribe", + "dev.soupslurpr.transcribro" + ) + + if(!privacyWhitelist.contains(externalPkg.value)) + Tip(stringResource(R.string.voice_input_settings_backend_system_external_warning, + currOption?.name ?: externalPkg.value)) + } + }, //if(!systemVoiceInput.value) { userSettingToggleDataStore( From d9e96a33d126959af226ea25d372e172bedd1b60 Mon Sep 17 00:00:00 2001 From: Aleksandras Kostarevas Date: Sat, 5 Sep 2026 20:14:39 +0200 Subject: [PATCH 4/5] Fix packageinfo crash --- .../inputmethod/latin/uix/settings/pages/VoiceInput.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt index 51544db768..405ce1e807 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt @@ -42,8 +42,13 @@ private data class VoiceIMEInfo( fun usePackageReadableName(pkg: String): String? { val context = LocalContext.current return remember(pkg) { - context.packageManager.getPackageInfo(pkg, 0)?.applicationInfo?.let { - context.packageManager.getApplicationLabel(it).toString() + try { + context.packageManager.getPackageInfo(pkg, 0) + } catch(e: Exception) { + null + }?.applicationInfo?.let { + context.packageManager.getApplicationLabel(it).toString() + } } } } From a0b84ef9a349273040e36fbe5fec5c45bc62dd28 Mon Sep 17 00:00:00 2001 From: Aleksandras Kostarevas Date: Sat, 5 Sep 2026 20:26:21 +0200 Subject: [PATCH 5/5] Fix braces --- .../futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt index 405ce1e807..29ac803324 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/VoiceInput.kt @@ -47,8 +47,7 @@ fun usePackageReadableName(pkg: String): String? { } catch(e: Exception) { null }?.applicationInfo?.let { - context.packageManager.getApplicationLabel(it).toString() - } + context.packageManager.getApplicationLabel(it).toString() } } }