Refactor voice input detection and preference structure

This commit is contained in:
Rocka 2026-03-11 01:48:36 +08:00
parent 4a4ae575ed
commit 6636e322bd
No known key found for this signature in database
GPG Key ID: 28031158FFDD6853
6 changed files with 100 additions and 69 deletions

View File

@ -4,14 +4,11 @@
*/
package org.fcitx.fcitx5.android.data.prefs
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import androidx.annotation.Keep
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.content.edit
import androidx.preference.ListPreference
import androidx.preference.PreferenceManager
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.data.InputFeedbacks.InputFeedbackMode
@ -25,7 +22,6 @@ import org.fcitx.fcitx5.android.input.keyboard.SwipeSymbolDirection
import org.fcitx.fcitx5.android.input.picker.PickerWindow
import org.fcitx.fcitx5.android.input.popup.EmojiModifier
import org.fcitx.fcitx5.android.utils.DeviceUtil
import org.fcitx.fcitx5.android.utils.InputMethodUtil
import org.fcitx.fcitx5.android.utils.appContext
import org.fcitx.fcitx5.android.utils.vibrator
@ -144,50 +140,12 @@ 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: ManagedPreference.PString
init {
fun voiceInputPreference(
@StringRes
title: Int,
key: String,
defaultValue: String,
enableUiOn: (() -> Boolean)? = null
): ManagedPreference.PString {
val pref = ManagedPreference.PString(sharedPreferences, key, defaultValue)
val ui = object : ManagedPreferenceUi<ListPreference>(key, enableUiOn) {
override fun createUi(context: Context): ListPreference {
val ui = ListPreference(context)
ui.key = key
val voiceInputMethods = InputMethodUtil.voiceInputMethods()
val values = voiceInputMethods.map { it.first.packageName }
val labels = voiceInputMethods.map { it.first.loadLabel(context.packageManager) }
return ui.apply {
isIconSpaceReserved = false
isSingleLineTitle = false
entryValues = values.toTypedArray()
summaryProvider = ListPreference.SimpleSummaryProvider.getInstance()
setDefaultValue(defaultValue)
setTitle(title)
entries = labels.toTypedArray()
setDialogTitle(title)
}
}
}
pref.register()
ui.registerUi()
return pref
}
preferredVoiceInput = voiceInputPreference(
R.string.preferred_voice_input,
"preferred_voice_input",
""
) { showVoiceInputButton.getValue() }
}
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)

View File

@ -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,

View File

@ -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<T : Preference>(
val key: String,
@ -67,6 +70,38 @@ abstract class ManagedPreferenceUi<T : Preference>(
}
}
class VoiceInputList(
@StringRes
val title: Int,
key: String,
val defaultValue: String,
enableUiOn: (() -> Boolean)? = null
) : ManagedPreferenceUi<ListPreference>(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<ListPreference> { preference ->
if (preference.entryValues.includes(preference.value)) {
preference.entry
} else {
context.getString(R.string._not_available_)
}
}
}
}
class EditTextInt(
@StringRes
val title: Int,

View File

@ -373,15 +373,7 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
idleUi.inlineSuggestionsBar.clear()
}
voiceInputSubtype = if (preferredVoiceInput.isNotEmpty()) {
InputMethodUtil.voiceInputMethods().find {
it.first.packageName == preferredVoiceInput
}?.let { (info, subType) ->
info.id to subType
} ?: InputMethodUtil.firstVoiceInput()
} else {
InputMethodUtil.firstVoiceInput()
}
voiceInputSubtype = InputMethodUtil.findVoiceSubtype(preferredVoiceInput)
val shouldShowVoiceInput =
showVoiceInputButton && voiceInputSubtype != null && !capFlags.has(CapabilityFlag.Password)
idleUi.setHideKeyboardIsVoiceInput(

View File

@ -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
}

View File

@ -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
@ -46,23 +46,36 @@ object InputMethodUtil {
fun showPicker() = appContext.inputMethodManager.showInputMethodPicker()
fun voiceInputMethods(): List<Pair<InputMethodInfo, InputMethodSubtype>> =
appContext.inputMethodManager
.enabledInputMethodList
fun listVoiceInputMethods(): List<Pair<InputMethodInfo, InputMethodSubtype>> {
return appContext.inputMethodManager.enabledInputMethodList
.mapNotNull { info ->
for (i in 0 until info.subtypeCount) {
val subType = info.getSubtypeAt(i)
if (subType.mode.lowercase() == "voice") {
return@mapNotNull info to subType
}
}
return@mapNotNull null
info.firstVoiceSubtype()?.let { info to it }
}
}
fun firstVoiceInput(): Pair<String, InputMethodSubtype>? =
voiceInputMethods().firstNotNullOfOrNull { (info, subType) ->
info.id to subType
/**
* Find input method with `"voice"` subtype, preferring one with [id]
*/
fun findVoiceSubtype(id: String): Pair<String, InputMethodSubtype>? {
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,