added local implementation of speech_to_text_dialog

This commit is contained in:
Fabian Freund
2026-01-13 12:07:32 +01:00
parent 849823f5df
commit 565595a0be
47 changed files with 1612 additions and 19 deletions
@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.weblibre.speech_to_text_dialog">
</manifest>
@@ -0,0 +1,151 @@
/*
* Copyright (c) 2024-2025 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.weblibre.speech_to_text_dialog
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.speech.RecognizerIntent
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.PluginRegistry
import eu.weblibre.speech_to_text_dialog.pigeons.SpeechToTextApi as PigeonSpeechToTextApi
import eu.weblibre.speech_to_text_dialog.pigeons.SpeechToTextEvents
import java.util.Locale
/// Plugin implementation for speech recognition dialog.
///
/// This plugin integrates Android's RecognizerIntent to provide
/// speech-to-text functionality to Flutter apps.
class SpeechToTextDialogPlugin : FlutterPlugin, ActivityAware,
PluginRegistry.ActivityResultListener {
companion object {
private const val REQ_CODE_SPEECH_INPUT = 120752
}
private var activity: Activity? = null
private var speechApiHost: SpeechApiHost? = null
override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
speechApiHost = SpeechApiHost(binding.binaryMessenger, this)
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
speechApiHost?.release()
speechApiHost = null
}
override fun onAttachedToActivity(@NonNull binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(@NonNull binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivity() {
activity = null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == REQ_CODE_SPEECH_INPUT) {
speechApiHost?.handleActivityResult(resultCode, data)
return true
}
return false
}
/// Show the speech recognition dialog.
///
/// @param locale The locale string (e.g., "en-US", "de-DE") or null for device default.
/// @return true if the dialog was shown successfully, false otherwise.
fun showSpeechDialog(locale: String?): Boolean {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
)
putExtra(
RecognizerIntent.EXTRA_LANGUAGE,
locale?.let { Locale(it) } ?: Locale.getDefault()
)
putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak")
}
return try {
activity?.startActivityForResult(intent, REQ_CODE_SPEECH_INPUT)
true
} catch (e: ActivityNotFoundException) {
false
}
}
}
/// Host API implementation that handles Flutter-to-Android calls.
class SpeechApiHost(
private val messenger: BinaryMessenger,
private val plugin: SpeechToTextDialogPlugin
) {
private val events: SpeechToTextEvents = SpeechToTextEvents(messenger)
init {
// Set up the API handler
PigeonSpeechToTextApi.setUp(messenger, object : PigeonSpeechToTextApi {
override fun showDialog(locale: String?): Boolean {
return plugin.showSpeechDialog(locale)
}
})
}
/// Handle the result from the speech recognition activity.
fun handleActivityResult(resultCode: Int, data: Intent?) {
sendResult(resultCode, data)
}
private fun sendResult(resultCode: Int, data: Intent?) {
val text = if (resultCode == Activity.RESULT_OK && data != null) {
val result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
if (!result.isNullOrEmpty()) {
result[0]
} else {
""
}
} else {
// User cancelled or recognition failed
""
}
events.onTextReceived(text) { }
}
fun release() {
PigeonSpeechToTextApi.setUp(messenger, null)
}
}
@@ -0,0 +1,143 @@
// Autogenerated from Pigeon (v26.1.5), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
package eu.weblibre.speech_to_text_dialog.pigeons
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMethodCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private object SpeechToTextPigeonUtils {
fun createConnectionError(channelName: String): FlutterError {
return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") }
fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
fun wrapError(exception: Throwable): List<Any?> {
return if (exception is FlutterError) {
listOf(
exception.code,
exception.message,
exception.details
)
} else {
listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
private open class SpeechToTextPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return super.readValueOfType(type, buffer)
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
super.writeValue(stream, value)
}
}
/**
* Host API - methods called from Flutter to native Android.
*
* Generated interface from Pigeon that represents a handler of messages from Flutter.
*/
interface SpeechToTextApi {
/**
* Show the speech recognition dialog.
*
* Returns [true] if the dialog was shown successfully.
* Returns [false] if the speech recognition service is not available.
*
* The [locale] parameter specifies the language locale for recognition
* (e.g., 'en-US', 'de-DE'). If null, uses the device default.
*/
fun showDialog(locale: String?): Boolean
companion object {
/** The codec used by SpeechToTextApi. */
val codec: MessageCodec<Any?> by lazy {
SpeechToTextPigeonCodec()
}
/** Sets up an instance of `SpeechToTextApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: SpeechToTextApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val localeArg = args[0] as String?
val wrapped: List<Any?> = try {
listOf(api.showDialog(localeArg))
} catch (exception: Throwable) {
SpeechToTextPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
/**
* Flutter API - callbacks from native Android to Flutter.
*
* Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.
*/
class SpeechToTextEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by SpeechToTextEvents. */
val codec: MessageCodec<Any?> by lazy {
SpeechToTextPigeonCodec()
}
}
/**
* Called when speech recognition completes with the recognized text.
*
* [text] contains the recognized speech text. May be empty if
* recognition failed or was cancelled.
*/
fun onTextReceived(textArg: String, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(textArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(SpeechToTextPigeonUtils.createConnectionError(channelName)))
}
}
}
}
@@ -0,0 +1,27 @@
package eu.weblibre.speech_to_text_dialog
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import org.mockito.Mockito
import kotlin.test.Test
/*
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
*
* Once you have built the plugin's example app, you can run these tests from the command
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
* you can run them directly from IDEs that support JUnit such as Android Studio.
*/
internal class SpeechToTextDialogPluginTest {
@Test
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
val plugin = SpeechToTextDialogPlugin()
val call = MethodCall("getPlatformVersion", null)
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
plugin.onMethodCall(call, mockResult)
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
}
}