From 565595a0becf0b4f31087b5b992715b017f16b75 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Tue, 13 Jan 2026 12:07:32 +0100 Subject: [PATCH] added local implementation of speech_to_text_dialog --- .../widgets/speech_to_text_button.dart | 61 +++++-- app/pubspec.yaml | 5 +- lib/src/pigeons/speech_to_text.g.dart | 137 ++++++++++++++++ packages/speech_to_text_dialog/.gitignore | 33 ++++ packages/speech_to_text_dialog/.metadata | 30 ++++ packages/speech_to_text_dialog/CHANGELOG.md | 3 + packages/speech_to_text_dialog/LICENSE | 1 + packages/speech_to_text_dialog/README.md | 15 ++ .../analysis_options.yaml | 4 + .../speech_to_text_dialog/android/.gitignore | 9 ++ .../android/build.gradle | 66 ++++++++ .../android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../SpeechToTextDialogPlugin.kt | 151 ++++++++++++++++++ .../pigeons/SpeechToText.g.kt | 143 +++++++++++++++++ .../SpeechToTextDialogPluginTest.kt | 27 ++++ .../speech_to_text_dialog/example/.gitignore | 45 ++++++ .../speech_to_text_dialog/example/README.md | 16 ++ .../example/analysis_options.yaml | 28 ++++ .../example/android/.gitignore | 14 ++ .../example/android/app/build.gradle.kts | 44 +++++ .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 45 ++++++ .../MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 ++ .../main/res/drawable/launch_background.xml | 12 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 +++ .../app/src/main/res/values/styles.xml | 18 +++ .../app/src/profile/AndroidManifest.xml | 7 + .../example/android/build.gradle.kts | 24 +++ .../example/android/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../example/android/settings.gradle.kts | 26 +++ .../plugin_integration_test.dart | 33 ++++ .../example/lib/main.dart | 134 ++++++++++++++++ .../example/pubspec.yaml | 86 ++++++++++ .../example/test/widget_test.dart | 27 ++++ .../lib/speech_to_text_dialog.dart | 109 +++++++++++++ .../lib/src/pigeons/speech_to_text.g.dart | 137 ++++++++++++++++ .../pigeons/speech_to_text.dart | 60 +++++++ packages/speech_to_text_dialog/pubspec.yaml | 26 +++ pubspec.yaml | 2 + 47 files changed, 1612 insertions(+), 19 deletions(-) create mode 100644 lib/src/pigeons/speech_to_text.g.dart create mode 100644 packages/speech_to_text_dialog/.gitignore create mode 100644 packages/speech_to_text_dialog/.metadata create mode 100644 packages/speech_to_text_dialog/CHANGELOG.md create mode 100644 packages/speech_to_text_dialog/LICENSE create mode 100644 packages/speech_to_text_dialog/README.md create mode 100644 packages/speech_to_text_dialog/analysis_options.yaml create mode 100644 packages/speech_to_text_dialog/android/.gitignore create mode 100644 packages/speech_to_text_dialog/android/build.gradle create mode 100644 packages/speech_to_text_dialog/android/settings.gradle create mode 100644 packages/speech_to_text_dialog/android/src/main/AndroidManifest.xml create mode 100644 packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPlugin.kt create mode 100644 packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/pigeons/SpeechToText.g.kt create mode 100644 packages/speech_to_text_dialog/android/src/test/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPluginTest.kt create mode 100644 packages/speech_to_text_dialog/example/.gitignore create mode 100644 packages/speech_to_text_dialog/example/README.md create mode 100644 packages/speech_to_text_dialog/example/analysis_options.yaml create mode 100644 packages/speech_to_text_dialog/example/android/.gitignore create mode 100644 packages/speech_to_text_dialog/example/android/app/build.gradle.kts create mode 100644 packages/speech_to_text_dialog/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/speech_to_text_dialog/example/android/app/src/main/AndroidManifest.xml create mode 100644 packages/speech_to_text_dialog/example/android/app/src/main/kotlin/eu/weblibre/speech_to_text_dialog_example/MainActivity.kt create mode 100644 packages/speech_to_text_dialog/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 packages/speech_to_text_dialog/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/speech_to_text_dialog/example/android/app/src/main/res/values-night/styles.xml create mode 100644 packages/speech_to_text_dialog/example/android/app/src/main/res/values/styles.xml create mode 100644 packages/speech_to_text_dialog/example/android/app/src/profile/AndroidManifest.xml create mode 100644 packages/speech_to_text_dialog/example/android/build.gradle.kts create mode 100644 packages/speech_to_text_dialog/example/android/gradle.properties create mode 100644 packages/speech_to_text_dialog/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/speech_to_text_dialog/example/android/settings.gradle.kts create mode 100644 packages/speech_to_text_dialog/example/integration_test/plugin_integration_test.dart create mode 100644 packages/speech_to_text_dialog/example/lib/main.dart create mode 100644 packages/speech_to_text_dialog/example/pubspec.yaml create mode 100644 packages/speech_to_text_dialog/example/test/widget_test.dart create mode 100644 packages/speech_to_text_dialog/lib/speech_to_text_dialog.dart create mode 100644 packages/speech_to_text_dialog/lib/src/pigeons/speech_to_text.g.dart create mode 100644 packages/speech_to_text_dialog/pigeons/speech_to_text.dart create mode 100644 packages/speech_to_text_dialog/pubspec.yaml diff --git a/app/lib/presentation/widgets/speech_to_text_button.dart b/app/lib/presentation/widgets/speech_to_text_button.dart index 0ba0e3a5..79baea8d 100644 --- a/app/lib/presentation/widgets/speech_to_text_button.dart +++ b/app/lib/presentation/widgets/speech_to_text_button.dart @@ -17,31 +17,60 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'dart:async'; + import 'package:flutter/material.dart'; -import 'package:speech_to_text_google_dialog/speech_to_text_google_dialog.dart'; +import 'package:speech_to_text_dialog/speech_to_text_dialog.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; -class SpeechToTextButton extends StatelessWidget { - final Function(dynamic data) onTextReceived; +class SpeechToTextButton extends StatefulWidget { + final Function(String text) onTextReceived; const SpeechToTextButton({required this.onTextReceived, super.key}); + @override + State createState() => _SpeechToTextButtonState(); +} + +class _SpeechToTextButtonState extends State { + final _speechDialog = SpeechToTextDialog(); + StreamSubscription? _textSubscription; + + @override + void dispose() { + _textSubscription?.cancel().ignore(); + _speechDialog.dispose(); + super.dispose(); + } + + Future _showSpeechDialog(BuildContext context) async { + // Cancel any existing subscription + await _textSubscription?.cancel(); + + // Listen for the next text result + _textSubscription = _speechDialog.textStream.take(1).listen((text) { + if (text.isNotEmpty) { + widget.onTextReceived(text); + } + }); + + // Show the dialog + final isServiceAvailable = await _speechDialog.showDialog( + // locale: "en-US", + ); + + if (!isServiceAvailable) { + if (context.mounted) { + ui_helper.showErrorMessage(context, 'Service is not available'); + } + await _textSubscription?.cancel(); + } + } + @override Widget build(BuildContext context) { return IconButton( - onPressed: () async { - final isServiceAvailable = await SpeechToTextGoogleDialog.getInstance() - .showGoogleDialog( - onTextReceived: onTextReceived, - // locale: "en-US", - ); - - if (!isServiceAvailable) { - if (context.mounted) { - ui_helper.showErrorMessage(context, 'Service is not available'); - } - } - }, + onPressed: () => _showSpeechDialog(context), icon: const Icon(Icons.mic), ); } diff --git a/app/pubspec.yaml b/app/pubspec.yaml index d4c54f8b..118a4d22 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -77,9 +77,8 @@ dependencies: sliver_tools: ^0.2.12 smooth_page_indicator: ^2.0.1 socks5_proxy: ^2.1.1 - speech_to_text_google_dialog: - git: - url: https://github.com/FaFre/speech_to_text_google_dialog.git + speech_to_text_dialog: + path: ../packages/speech_to_text_dialog sqlite3: ^2.9.4 sqlite3_flutter_libs: ^0.5.41 synchronized: ^3.4.0 diff --git a/lib/src/pigeons/speech_to_text.g.dart b/lib/src/pigeons/speech_to_text.g.dart new file mode 100644 index 00000000..124a8aee --- /dev/null +++ b/lib/src/pigeons/speech_to_text.g.dart @@ -0,0 +1,137 @@ +// Autogenerated from Pigeon (v26.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } +} + +/// Host API - methods called from Flutter to native Android. +class SpeechToTextApi { + /// Constructor for [SpeechToTextApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + SpeechToTextApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// 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. + Future showDialog({String? locale}) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([locale]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } +} + +/// Flutter API - callbacks from native Android to Flutter. +abstract class SpeechToTextEvents { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + /// Called when speech recognition completes with the recognized text. + /// + /// [text] contains the recognized speech text. May be empty if + /// recognition failed or was cancelled. + void onTextReceived(String text); + + static void setUp(SpeechToTextEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null.'); + final List args = (message as List?)!; + final String? arg_text = (args[0] as String?); + assert(arg_text != null, + 'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null, expected non-null String.'); + try { + api.onTextReceived(arg_text!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/packages/speech_to_text_dialog/.gitignore b/packages/speech_to_text_dialog/.gitignore new file mode 100644 index 00000000..b9d7f25b --- /dev/null +++ b/packages/speech_to_text_dialog/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins-dependencies +/build/ +/coverage/ diff --git a/packages/speech_to_text_dialog/.metadata b/packages/speech_to_text_dialog/.metadata new file mode 100644 index 00000000..7a9751c8 --- /dev/null +++ b/packages/speech_to_text_dialog/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + - platform: android + create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/speech_to_text_dialog/CHANGELOG.md b/packages/speech_to_text_dialog/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/packages/speech_to_text_dialog/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/speech_to_text_dialog/LICENSE b/packages/speech_to_text_dialog/LICENSE new file mode 100644 index 00000000..ba75c69f --- /dev/null +++ b/packages/speech_to_text_dialog/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/speech_to_text_dialog/README.md b/packages/speech_to_text_dialog/README.md new file mode 100644 index 00000000..2e2ccf12 --- /dev/null +++ b/packages/speech_to_text_dialog/README.md @@ -0,0 +1,15 @@ +# speech_to_text_dialog + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/to/develop-plugins), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/packages/speech_to_text_dialog/analysis_options.yaml b/packages/speech_to_text_dialog/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/speech_to_text_dialog/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/speech_to_text_dialog/android/.gitignore b/packages/speech_to_text_dialog/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/packages/speech_to_text_dialog/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/speech_to_text_dialog/android/build.gradle b/packages/speech_to_text_dialog/android/build.gradle new file mode 100644 index 00000000..651a1583 --- /dev/null +++ b/packages/speech_to_text_dialog/android/build.gradle @@ -0,0 +1,66 @@ +group = "eu.weblibre.speech_to_text_dialog" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.2.20" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.11.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "eu.weblibre.speech_to_text_dialog" + + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 24 + } + + dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/speech_to_text_dialog/android/settings.gradle b/packages/speech_to_text_dialog/android/settings.gradle new file mode 100644 index 00000000..21ea7340 --- /dev/null +++ b/packages/speech_to_text_dialog/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'speech_to_text_dialog' diff --git a/packages/speech_to_text_dialog/android/src/main/AndroidManifest.xml b/packages/speech_to_text_dialog/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..38d63fa9 --- /dev/null +++ b/packages/speech_to_text_dialog/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPlugin.kt b/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPlugin.kt new file mode 100644 index 00000000..2f9c795a --- /dev/null +++ b/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPlugin.kt @@ -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 . + */ +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) + } +} diff --git a/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/pigeons/SpeechToText.g.kt b/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/pigeons/SpeechToText.g.kt new file mode 100644 index 00000000..a00fa699 --- /dev/null +++ b/packages/speech_to_text_dialog/android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/pigeons/SpeechToText.g.kt @@ -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 { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + 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 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(binaryMessenger, "dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val localeArg = args[0] as String? + val wrapped: List = 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 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) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(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))) + } + } + } +} diff --git a/packages/speech_to_text_dialog/android/src/test/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPluginTest.kt b/packages/speech_to_text_dialog/android/src/test/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPluginTest.kt new file mode 100644 index 00000000..56cbc61b --- /dev/null +++ b/packages/speech_to_text_dialog/android/src/test/kotlin/eu/weblibre/speech_to_text_dialog/SpeechToTextDialogPluginTest.kt @@ -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) + } +} diff --git a/packages/speech_to_text_dialog/example/.gitignore b/packages/speech_to_text_dialog/example/.gitignore new file mode 100644 index 00000000..3820a95c --- /dev/null +++ b/packages/speech_to_text_dialog/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/speech_to_text_dialog/example/README.md b/packages/speech_to_text_dialog/example/README.md new file mode 100644 index 00000000..429d20f5 --- /dev/null +++ b/packages/speech_to_text_dialog/example/README.md @@ -0,0 +1,16 @@ +# speech_to_text_dialog_example + +Demonstrates how to use the speech_to_text_dialog plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/speech_to_text_dialog/example/analysis_options.yaml b/packages/speech_to_text_dialog/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/packages/speech_to_text_dialog/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/speech_to_text_dialog/example/android/.gitignore b/packages/speech_to_text_dialog/example/android/.gitignore new file mode 100644 index 00000000..be3943c9 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/packages/speech_to_text_dialog/example/android/app/build.gradle.kts b/packages/speech_to_text_dialog/example/android/app/build.gradle.kts new file mode 100644 index 00000000..a47fd1bf --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "eu.weblibre.speech_to_text_dialog_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "eu.weblibre.speech_to_text_dialog_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/speech_to_text_dialog/example/android/app/src/debug/AndroidManifest.xml b/packages/speech_to_text_dialog/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/AndroidManifest.xml b/packages/speech_to_text_dialog/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8e082513 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/kotlin/eu/weblibre/speech_to_text_dialog_example/MainActivity.kt b/packages/speech_to_text_dialog/example/android/app/src/main/kotlin/eu/weblibre/speech_to_text_dialog_example/MainActivity.kt new file mode 100644 index 00000000..754d90e1 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/kotlin/eu/weblibre/speech_to_text_dialog_example/MainActivity.kt @@ -0,0 +1,5 @@ +package eu.weblibre.speech_to_text_dialog_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable/launch_background.xml b/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/speech_to_text_dialog/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/values-night/styles.xml b/packages/speech_to_text_dialog/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/main/res/values/styles.xml b/packages/speech_to_text_dialog/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/speech_to_text_dialog/example/android/app/src/profile/AndroidManifest.xml b/packages/speech_to_text_dialog/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/speech_to_text_dialog/example/android/build.gradle.kts b/packages/speech_to_text_dialog/example/android/build.gradle.kts new file mode 100644 index 00000000..dbee657b --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/speech_to_text_dialog/example/android/gradle.properties b/packages/speech_to_text_dialog/example/android/gradle.properties new file mode 100644 index 00000000..fbee1d8c --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/packages/speech_to_text_dialog/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/speech_to_text_dialog/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..e4ef43fb --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/packages/speech_to_text_dialog/example/android/settings.gradle.kts b/packages/speech_to_text_dialog/example/android/settings.gradle.kts new file mode 100644 index 00000000..ca7fe065 --- /dev/null +++ b/packages/speech_to_text_dialog/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/packages/speech_to_text_dialog/example/integration_test/plugin_integration_test.dart b/packages/speech_to_text_dialog/example/integration_test/plugin_integration_test.dart new file mode 100644 index 00000000..6ebb6c10 --- /dev/null +++ b/packages/speech_to_text_dialog/example/integration_test/plugin_integration_test.dart @@ -0,0 +1,33 @@ +// This is a basic Flutter integration test. +// +// Since integration tests run in a full Flutter application, they can interact +// with the host side of a plugin implementation, unlike Dart unit tests. +// +// For more information about Flutter integration tests, please see +// https://flutter.dev/to/integration-testing + + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:speech_to_text_dialog/speech_to_text_dialog.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('showDialog returns bool', (WidgetTester tester) async { + final SpeechToTextDialog plugin = SpeechToTextDialog(); + final bool result = await plugin.showDialog(); + // The result indicates if the dialog was shown successfully. + // In a test environment without proper activity, this may return false. + expect(result, isA()); + plugin.dispose(); + }); + + testWidgets('textStream emits String values', (WidgetTester tester) async { + final SpeechToTextDialog plugin = SpeechToTextDialog(); + final Stream stream = plugin.textStream; + expect(stream, isA>()); + plugin.dispose(); + }); +} diff --git a/packages/speech_to_text_dialog/example/lib/main.dart b/packages/speech_to_text_dialog/example/lib/main.dart new file mode 100644 index 00000000..efa4c7e7 --- /dev/null +++ b/packages/speech_to_text_dialog/example/lib/main.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:speech_to_text_dialog/speech_to_text_dialog.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final _speechToTextDialog = SpeechToTextDialog(); + StreamSubscription? _textSubscription; + String _recognizedText = 'Tap the button to start speaking...'; + String _selectedLocale = 'en-US'; + + final List _locales = [ + 'en-US', + 'de-DE', + 'es-ES', + 'fr-FR', + 'it-IT', + 'pt-BR', + 'ru-RU', + 'zh-CN', + 'ja-JP', + 'ko-KR', + ]; + + @override + void initState() { + super.initState(); + // Listen for recognized text + _textSubscription = _speechToTextDialog.textStream.listen((text) { + setState(() { + if (text.isEmpty) { + _recognizedText = 'No speech detected or cancelled'; + } else { + _recognizedText = text; + } + }); + }); + } + + @override + void dispose() { + _textSubscription?.cancel(); + _speechToTextDialog.dispose(); + super.dispose(); + } + + Future _startListening() async { + setState(() { + _recognizedText = 'Listening...'; + }); + + final success = await _speechToTextDialog.showDialog( + locale: _selectedLocale, + ); + + if (!success) { + setState(() { + _recognizedText = 'Speech recognition not available'; + }); + } + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Speech to Text Dialog'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.mic, + size: 80, + color: Colors.blue, + ), + const SizedBox(height: 24), + Text( + _recognizedText, + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + DropdownButton( + value: _selectedLocale, + hint: const Text('Select Language'), + items: _locales.map((String locale) { + return DropdownMenuItem( + value: locale, + child: Text(locale), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + setState(() { + _selectedLocale = newValue; + }); + } + }, + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _startListening, + icon: const Icon(Icons.mic), + label: const Text('Start Speech Recognition'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/packages/speech_to_text_dialog/example/pubspec.yaml b/packages/speech_to_text_dialog/example/pubspec.yaml new file mode 100644 index 00000000..3da990ec --- /dev/null +++ b/packages/speech_to_text_dialog/example/pubspec.yaml @@ -0,0 +1,86 @@ +name: speech_to_text_dialog_example +description: "Demonstrates how to use the speech_to_text_dialog plugin." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' +resolution: workspace + +environment: + sdk: ^3.10.4 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + speech_to_text_dialog: + # When depending on this package from a real application you should use: + # speech_to_text_dialog: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/packages/speech_to_text_dialog/example/test/widget_test.dart b/packages/speech_to_text_dialog/example/test/widget_test.dart new file mode 100644 index 00000000..d72f4d27 --- /dev/null +++ b/packages/speech_to_text_dialog/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:speech_to_text_dialog_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/packages/speech_to_text_dialog/lib/speech_to_text_dialog.dart b/packages/speech_to_text_dialog/lib/speech_to_text_dialog.dart new file mode 100644 index 00000000..c29f1e23 --- /dev/null +++ b/packages/speech_to_text_dialog/lib/speech_to_text_dialog.dart @@ -0,0 +1,109 @@ +/* + * 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 . + */ + +import 'dart:async'; +import 'package:flutter/services.dart' show BinaryMessenger; +import 'src/pigeons/speech_to_text.g.dart'; + +/// Speech recognition dialog using Android's RecognizerIntent. +/// +/// This class provides a Flutter interface to Android's speech recognition +/// dialog, allowing users to dictate text that is then returned to the app. +/// +/// Example usage: +/// ```dart +/// final speechDialog = SpeechToTextDialog(); +/// +/// // Listen for results +/// final subscription = speechDialog.textStream.listen((text) { +/// print('Recognized: $text'); +/// }); +/// +/// // Show the dialog +/// final success = await speechDialog.showDialog(locale: 'en-US'); +/// +/// // Clean up when done +/// await subscription.cancel(); +/// speechDialog.dispose(); +/// ``` +class SpeechToTextDialog implements SpeechToTextEvents { + /// Creates a new [SpeechToTextDialog] instance. + /// + /// Optionally provide a custom [api] for testing or dependency injection. + /// If [binaryMessenger] is provided, it will be used for Pigeon communication. + SpeechToTextDialog({ + SpeechToTextApi? api, + BinaryMessenger? binaryMessenger, + }) : _api = api ?? SpeechToTextApi(binaryMessenger: binaryMessenger), + _binaryMessenger = binaryMessenger { + // Set up the event handler + SpeechToTextEvents.setUp(this, binaryMessenger: binaryMessenger); + } + + final SpeechToTextApi _api; + final BinaryMessenger? _binaryMessenger; + final _textStreamController = StreamController.broadcast(); + bool _disposed = false; + + /// Stream of recognized speech text. + /// + /// This stream emits the recognized text when the user completes + /// speech input. An empty string may be emitted if recognition failed + /// or the user cancelled. + Stream get textStream => _textStreamController.stream; + + /// 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', 'es-ES'). If null, uses the device default locale. + /// + /// Example: + /// ```dart + /// final success = await speechDialog.showDialog(locale: 'en-US'); + /// if (!success) { + /// print('Speech recognition not available'); + /// } + /// ``` + Future showDialog({String? locale}) async { + return _api.showDialog(locale: locale); + } + + @override + void onTextReceived(String text) { + if (!_disposed) { + _textStreamController.add(text); + } + } + + /// Release resources used by this instance. + /// + /// Call this when the speech dialog is no longer needed to avoid memory leaks. + /// After calling [dispose], this instance cannot be used again. + void dispose() { + if (!_disposed) { + _disposed = true; + SpeechToTextEvents.setUp(null, binaryMessenger: _binaryMessenger); + _textStreamController.close(); + } + } +} diff --git a/packages/speech_to_text_dialog/lib/src/pigeons/speech_to_text.g.dart b/packages/speech_to_text_dialog/lib/src/pigeons/speech_to_text.g.dart new file mode 100644 index 00000000..124a8aee --- /dev/null +++ b/packages/speech_to_text_dialog/lib/src/pigeons/speech_to_text.g.dart @@ -0,0 +1,137 @@ +// Autogenerated from Pigeon (v26.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } +} + +/// Host API - methods called from Flutter to native Android. +class SpeechToTextApi { + /// Constructor for [SpeechToTextApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + SpeechToTextApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// 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. + Future showDialog({String? locale}) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([locale]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } +} + +/// Flutter API - callbacks from native Android to Flutter. +abstract class SpeechToTextEvents { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + /// Called when speech recognition completes with the recognized text. + /// + /// [text] contains the recognized speech text. May be empty if + /// recognition failed or was cancelled. + void onTextReceived(String text); + + static void setUp(SpeechToTextEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null.'); + final List args = (message as List?)!; + final String? arg_text = (args[0] as String?); + assert(arg_text != null, + 'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null, expected non-null String.'); + try { + api.onTextReceived(arg_text!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/packages/speech_to_text_dialog/pigeons/speech_to_text.dart b/packages/speech_to_text_dialog/pigeons/speech_to_text.dart new file mode 100644 index 00000000..245254e7 --- /dev/null +++ b/packages/speech_to_text_dialog/pigeons/speech_to_text.dart @@ -0,0 +1,60 @@ +/* + * 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 . + */ +import 'package:pigeon/pigeon.dart'; + +/// Pigeon API for speech recognition dialog. +/// +/// This API provides type-safe communication between Flutter and Android +/// for showing the Google speech recognition dialog. +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/pigeons/speech_to_text.g.dart', + dartOptions: DartOptions(), + kotlinOut: + 'android/src/main/kotlin/eu/weblibre/speech_to_text_dialog/pigeons/SpeechToText.g.kt', + kotlinOptions: KotlinOptions( + package: 'eu.weblibre.speech_to_text_dialog.pigeons', + ), + dartPackageName: 'speech_to_text_dialog', + ), +) + +/// Host API - methods called from Flutter to native Android. +@HostApi() +abstract class 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. + bool showDialog({String? locale}); +} + +/// Flutter API - callbacks from native Android to Flutter. +@FlutterApi() +abstract class SpeechToTextEvents { + /// Called when speech recognition completes with the recognized text. + /// + /// [text] contains the recognized speech text. May be empty if + /// recognition failed or was cancelled. + void onTextReceived(String text); +} diff --git a/packages/speech_to_text_dialog/pubspec.yaml b/packages/speech_to_text_dialog/pubspec.yaml new file mode 100644 index 00000000..d2463b22 --- /dev/null +++ b/packages/speech_to_text_dialog/pubspec.yaml @@ -0,0 +1,26 @@ +name: speech_to_text_dialog +description: Speech recognition dialog using Android's RecognizerIntent with type-safe Pigeon channels. +version: 0.0.1 +publish_to: 'none' +resolution: workspace + +environment: + sdk: ^3.10.4 + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + pigeon: ^26.1.5 + +flutter: + plugin: + platforms: + android: + package: eu.weblibre.speech_to_text_dialog + pluginClass: SpeechToTextDialogPlugin diff --git a/pubspec.yaml b/pubspec.yaml index ef4cd98e..d0b48117 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,8 @@ workspace: - packages/simple_intent_receiver/example - packages/locale_resolver - packages/locale_resolver/example + - packages/speech_to_text_dialog + - packages/speech_to_text_dialog/example dev_dependencies: melos: ^7.3.0