diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt index d108678d..5ec27647 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt @@ -141,8 +141,13 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { "keep_keyboard_letters_uppercase", false ) + val showVoiceInputButton = switch(R.string.show_voice_input_button, "show_voice_input_button", false) + val preferredVoiceInput = voiceInputPreference( + R.string.preferred_voice_input, "preferred_voice_input", "" + ) { showVoiceInputButton.getValue() } + val expandKeypressArea = switch(R.string.expand_keypress_area, "expand_keypress_area", false) val swipeSymbolDirection = enumList( diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceCategory.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceCategory.kt index 1c8a8499..0d4a350c 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceCategory.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceCategory.kt @@ -64,6 +64,20 @@ abstract class ManagedPreferenceCategory( return list(title, key, defaultValue, codec, entryValues, entryLabels, enableUiOn) } + protected fun voiceInputPreference( + @StringRes + title: Int, + key: String, + defaultValue: String, + enableUiOn: (() -> Boolean)? = null + ): ManagedPreference.PString { + val pref = ManagedPreference.PString(sharedPreferences, key, defaultValue) + val ui = ManagedPreferenceUi.VoiceInputList(title, key, defaultValue, enableUiOn) + pref.register() + ui.registerUi() + return pref + } + protected fun int( @StringRes title: Int, diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceUi.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceUi.kt index f18c56ae..c9a38b98 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceUi.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/ManagedPreferenceUi.kt @@ -9,10 +9,13 @@ import androidx.annotation.StringRes import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.Preference +import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.ui.main.modified.MySwitchPreference import org.fcitx.fcitx5.android.ui.main.settings.DialogSeekBarPreference import org.fcitx.fcitx5.android.ui.main.settings.EditTextIntPreference import org.fcitx.fcitx5.android.ui.main.settings.TwinSeekBarPreference +import org.fcitx.fcitx5.android.utils.InputMethodUtil +import org.fcitx.fcitx5.android.utils.includes abstract class ManagedPreferenceUi( val key: String, @@ -67,6 +70,38 @@ abstract class ManagedPreferenceUi( } } + class VoiceInputList( + @StringRes + val title: Int, + key: String, + val defaultValue: String, + enableUiOn: (() -> Boolean)? = null + ) : ManagedPreferenceUi(key, enableUiOn) { + override fun createUi(context: Context) = ListPreference(context).apply { + key = this@VoiceInputList.key + isIconSpaceReserved = false + isSingleLineTitle = false + summaryProvider = ListPreference.SimpleSummaryProvider.getInstance() + setDefaultValue(defaultValue) + setTitle(this@VoiceInputList.title) + setDialogTitle(this@VoiceInputList.title) + val voiceInputMethods = InputMethodUtil.listVoiceInputMethods() + entryValues = arrayOf("", *voiceInputMethods.map { it.first.id }.toTypedArray()) + entries = arrayOf( + context.getString(R.string.system_default), + *voiceInputMethods.map { it.first.loadLabel(context.packageManager) }.toTypedArray() + ) + // shows "(Not Available)" if selected id is not present + summaryProvider = Preference.SummaryProvider { preference -> + if (preference.entryValues.includes(preference.value)) { + preference.entry + } else { + context.getString(R.string._not_available_) + } + } + } + } + class EditTextInt( @StringRes val title: Int, diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index 0b2e96ee..ab15c201 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -105,6 +105,7 @@ class KawaiiBarComponent : UniqueViewComponent( private val expandToolbarByDefault by prefs.keyboard.expandToolbarByDefault private val toolbarNumRowOnPassword by prefs.keyboard.toolbarNumRowOnPassword private val showVoiceInputButton by prefs.keyboard.showVoiceInputButton + private val preferredVoiceInput by prefs.keyboard.preferredVoiceInput private var clipboardTimeoutJob: Job? = null @@ -450,7 +451,7 @@ class KawaiiBarComponent : UniqueViewComponent( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { idleUi.inlineSuggestionsBar.clear() } - voiceInputSubtype = InputMethodUtil.firstVoiceInput() + voiceInputSubtype = InputMethodUtil.findVoiceSubtype(preferredVoiceInput) val shouldShowVoiceInput = showVoiceInputButton && voiceInputSubtype != null && !capFlags.has(CapabilityFlag.Password) idleUi.setHideKeyboardIsVoiceInput( diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/InputMethodInfo.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/InputMethodInfo.kt new file mode 100644 index 00000000..1c814135 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/utils/InputMethodInfo.kt @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * SPDX-FileCopyrightText: Copyright 2026 Fcitx5 for Android Contributors + */ + +package org.fcitx.fcitx5.android.utils + +import android.view.inputmethod.InputMethodInfo +import android.view.inputmethod.InputMethodSubtype + +fun InputMethodInfo.firstVoiceSubtype() : InputMethodSubtype? { + for (index in 0 until subtypeCount) { + val subtype = getSubtypeAt(index) + if (subtype.mode.lowercase() == "voice") { + return subtype + } + } + return null +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/utils/InputMethodUtil.kt b/app/src/main/java/org/fcitx/fcitx5/android/utils/InputMethodUtil.kt index 3f3f9d30..3f58db90 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/utils/InputMethodUtil.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/utils/InputMethodUtil.kt @@ -1,6 +1,6 @@ /* * SPDX-License-Identifier: LGPL-2.1-or-later - * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors + * SPDX-FileCopyrightText: Copyright 2021-2026 Fcitx5 for Android Contributors */ package org.fcitx.fcitx5.android.utils @@ -9,6 +9,7 @@ import android.content.Context import android.content.Intent import android.os.Build import android.provider.Settings +import android.view.inputmethod.InputMethodInfo import android.view.inputmethod.InputMethodSubtype import org.fcitx.fcitx5.android.BuildConfig import org.fcitx.fcitx5.android.input.FcitxInputMethodService @@ -45,13 +46,36 @@ object InputMethodUtil { fun showPicker() = appContext.inputMethodManager.showInputMethodPicker() - fun firstVoiceInput(): Pair? = - appContext.inputMethodManager - .shortcutInputMethodsAndSubtypes - .firstNotNullOfOrNull { - it.value.find { subType -> subType.mode.lowercase() == "voice" } - ?.let { subType -> it.key.id to subType } + fun listVoiceInputMethods(): List> { + return appContext.inputMethodManager.enabledInputMethodList + .mapNotNull { info -> + info.firstVoiceSubtype()?.let { info to it } } + } + + /** + * Find input method with `"voice"` subtype, preferring one with [id] + */ + fun findVoiceSubtype(id: String): Pair? { + val inputMethods = appContext.inputMethodManager.enabledInputMethodList + if (inputMethods.isEmpty()) return null + var firstId: String? = null + var firstSubtype: InputMethodSubtype? = null + inputMethods.forEach { + val voiceSubtype = it.firstVoiceSubtype() ?: return@forEach + if (it.id == id) { + return id to voiceSubtype + } + if (firstId == null) { + firstId = it.id + firstSubtype = voiceSubtype + } + } + if (firstId != null && firstSubtype != null) { + return firstId to firstSubtype + } + return null + } fun switchInputMethod( service: FcitxInputMethodService, diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 596cadd1..d2c3c348 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -316,4 +316,5 @@ 🏿 深肤色 (Type-6) 隐藏不支持的表情 启用按键边框描边 + 首选语音输入 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ca0471d6..fa9bc4e7 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -315,4 +315,5 @@ 🏾 中等深膚色 (Type-5) 🏿 深膚色 (Type-6) 隱藏不支援的表情 - + 首選語音輸入 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ba4cc9ae..c98470cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -316,4 +316,5 @@ 🏿 Type-6 Hide unsupported Emojis Enable stroke for key border + Preferred voice input