Browse Source

Merge branch 'dev' of github.com:deepcloud2048/deep_voice into dev

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
54806c30e1
  1. 202
      lib/core/utils/permission_util.dart
  2. 6
      lib/data/services/asr_service.dart
  3. 13
      lib/data/services/ble_manager.dart
  4. 26
      lib/data/services/speech_impl/azure_asr_service.dart
  5. 186
      lib/modules/agent/controllers/agent_controller.dart
  6. 103
      lib/modules/agent/views/message_bubble.dart
  7. 50
      lib/modules/explore/controllers/explore_controller.dart
  8. 20
      lib/modules/home/views/home_view.dart
  9. 66
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  10. 175
      lib/modules/ota_upgrade/views/ota_upgrade_view.dart
  11. 92
      lib/modules/pairing/controllers/pairing_controller.dart
  12. 225
      lib/modules/settings/views/permissions_view.dart
  13. 20
      lib/modules/translation/controllers/translation_controller.dart
  14. 6
      lib/modules/translation/views/translation_view.dart
  15. 2
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  16. 1
      local_plugins/azure_speech/android/build.gradle.kts
  17. 370
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  18. 833
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  19. 5
      local_plugins/azure_speech/pubspec.yaml
  20. 22
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  21. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  22. 2
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties
  23. 26
      local_plugins/ota/android/src/main/kotlin/com/example/ota/DeviceConnection.kt
  24. 40
      local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt
  25. 3
      local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleManager.java
  26. 94
      local_plugins/ota/lib/ota.dart
  27. 1
      local_plugins/ota/pubspec.yaml
  28. 2
      pubspec.yaml

202
lib/core/utils/permission_util.dart

@ -0,0 +1,202 @@
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
import 'dart:developer';
import 'logger.dart';
// class Logger {
// static void e(String tag, String message) {
// debugPrint('[$tag] ERROR: $message');
// }
// }
class PermissionUtil {
static final PermissionUtil instance = PermissionUtil._();
PermissionUtil._();
OverlayEntry? _explanationOverlayEntry;
Future<bool> requestPermission({
required BuildContext context,
required Permission permissionType,
required String permissionName,
required String explanationText,
required String permanentDenialText,
String settingsButtonText = '前往设置',
String cancelButtonText = '取消',
}) async {
try {
final PermissionStatus status = await permissionType.status;
log('初始权限状态 for $permissionName: $status');
// 处理已授权状态(包括授予状态和临时通知权限)
if (status == PermissionStatus.granted ||
status == PermissionStatus.provisional) {
log('$permissionName 权限已授予或临时授权。');
return true;
}
// 处理永久拒绝状态
if (status == PermissionStatus.permanentlyDenied) {
log('$permissionName 权限被永久拒绝,显示引导对话框。');
await _showPermanentDenialDialog(
context: context,
permissionName: permissionName,
permanentDenialText: permanentDenialText,
settingsButtonText: settingsButtonText,
cancelButtonText: cancelButtonText,
);
return false;
}
// 处理初始拒绝/未决定状态
if (status == PermissionStatus.denied) {
log('$permissionName 权限被拒绝或未决定,显示说明横幅并请求。');
_showExplanationOverlay(
context: context,
permissionName: permissionName,
explanationText: explanationText,
);
final PermissionStatus newStatus = await permissionType.request();
log('请求后 $permissionName 的新权限状态: $newStatus');
_hideExplanationOverlay();
// 处理请求后的状态
if (newStatus == PermissionStatus.granted ||
newStatus == PermissionStatus.provisional) {
log('$permissionName 权限在请求后被授予或临时授权。');
return true;
} else if (newStatus == PermissionStatus.permanentlyDenied) {
log('$permissionName 权限在请求后被永久拒绝,再次显示引导对话框。');
await _showPermanentDenialDialog(
context: context,
permissionName: permissionName,
permanentDenialText: permanentDenialText,
settingsButtonText: settingsButtonText,
cancelButtonText: cancelButtonText,
);
}
return false;
}
// 处理受限状态
if (status == PermissionStatus.restricted ||
status == PermissionStatus.limited) {
log('$permissionName 权限受限或受限访问: $status');
await _showRestrictedPermissionDialog(context, permissionName);
return false;
}
// 其他未知状态处理
log('$permissionName 处于未知状态: $status');
return false;
} catch (e) {
Logger.e('PermissionUtil', '请求 $permissionName 权限失败: $e');
_hideExplanationOverlay();
return false;
}
}
void _showExplanationOverlay({
required BuildContext context,
required String permissionName,
required String explanationText,
}) {
_hideExplanationOverlay(); // 确保移除旧叠加层
_explanationOverlayEntry = OverlayEntry(
builder: (context) => Positioned(
top: MediaQuery.of(context).padding.top + 10,
left: 16,
right: 16,
child: Material(
borderRadius: BorderRadius.circular(12),
elevation: 4,
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'需要$permissionName权限',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 8),
Text(
explanationText,
style: const TextStyle(fontSize: 14),
),
],
),
),
),
),
);
Future.delayed(Duration.zero, () {
if (_explanationOverlayEntry != null) {
Overlay.of(context).insert(_explanationOverlayEntry!);
log('权限说明叠加层已显示。');
}
});
}
void _hideExplanationOverlay() {
_explanationOverlayEntry?.remove();
_explanationOverlayEntry = null;
}
Future<void> _showPermanentDenialDialog({
required BuildContext context,
required String permissionName,
required String permanentDenialText,
required String settingsButtonText,
required String cancelButtonText,
}) async {
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text('需要$permissionName权限'),
content: Text(permanentDenialText),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(cancelButtonText),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
AppSettings.openAppSettings();
},
child: Text(settingsButtonText),
),
],
),
);
}
Future<void> _showRestrictedPermissionDialog(
BuildContext context, String permissionName) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('$permissionName权限受限'),
content: const Text('此功能在当前设备上不可用或受限,请检查设备设置或系统限制'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
),
);
}
}

6
lib/data/services/asr_service.dart

@ -29,6 +29,12 @@ abstract class AsrService {
/// 检查连续识别是否活跃
bool isContinuousRecognitionActive();
/// 开启录音
Future<bool> enableRecord();
/// 停止录音
Future<bool> stopRecord();
/// 释放资源
Future<void> dispose();
}

13
lib/data/services/ble_manager.dart

@ -179,6 +179,19 @@ class BleManager extends GetxService {
state == BleConnectionState.error) {
Logger.info('BLE设备已断开或发生错误');
_isConnected.value = false;
// 更新设备信息
_updateDeviceInfoIfNeeded(
batteryLevel: 0,
leftEarBattery: 0,
rightEarBattery: 0,
caseBattery: 0,
leftEarCharging: false,
rightEarCharging: false,
caseCharging: false,
firmwareVersion: '',
firmwareProductId: '',
firmwareProductColor: '',
);
}
}

26
lib/data/services/speech_impl/azure_asr_service.dart

@ -342,4 +342,28 @@ class AzureAsrService extends GetxService implements AsrService {
return false;
}
}
}
@override
Future<bool> enableRecord() async {
try {
final bool result = await _channel.invokeMethod('enableRecord');
return result;
} catch (e) {
Logger.error('开始录音: ${e.toString()}');
rethrow;
}
}
@override
Future<bool> stopRecord() async {
try {
final bool result = await _channel.invokeMethod('stopRecord');
return result;
} catch (e) {
Logger.error('停止录音: ${e.toString()}');
rethrow;
}
}
}

186
lib/modules/agent/controllers/agent_controller.dart

@ -16,6 +16,7 @@ import 'package:app_settings/app_settings.dart';
import 'package:deep_voice/core/utils/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_view/photo_view.dart';
import '../../../core/utils/permission_util.dart';
class Message {
final bool isUser; //标识信息是否来自用户还是AI
@ -697,7 +698,16 @@ class AgentController extends GetxController {
// 如果要切换到语音模式,需要先检查麦克风权限
if (isTextInputMode.value) {
final hasPermission = await _checkMicrophonePermission();
// 使用 PermissionUtil 检查麦克风权限
final hasPermission = await PermissionUtil.instance.requestPermission(
context: Get.context!,
permissionType: Permission.microphone,
permissionName: '麦克风',
explanationText: '语音识别与翻译功能需要访问麦克风。',
permanentDenialText: '您已永久拒绝麦克风权限,请前往系统设置开启权限。',
settingsButtonText: '去设置',
cancelButtonText: '取消',
);
if (!hasPermission) {
return; // 权限未获取,保持文本模式
}
@ -715,54 +725,54 @@ class AgentController extends GetxController {
}
}
// 检查麦克风权限
// 检查麦克风权限,返回true表示已授权,false表示未授权
Future<bool> _checkMicrophonePermission() async {
try {
// iOS平台直接返回true,不处理权限问题
if (Platform.isIOS) {
// 1.先获取当前麦克风权限状态
final micStatus = await Permission.microphone.status;
// 2.如果麦克风权限已授予,返回true
if (micStatus.isGranted) {
return true;
}
// 创建待请求权限列表
// 3.创建待请求权限列表(这里只请求麦克风权限)
List<Permission> permissionsToRequest = [];
// Android 需要考虑SDK版本不同的权限要求
permissionsToRequest.add(Permission.microphone);
// 批量请求所有需要的权限
// 4.弹出权限说明对话框(首次拒绝/还没授权时展示)
if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true);
} else {
_showPermissionDialog(false);
}
// 5.请求权限,并获取返回的权限状态集合
final statuses = await permissionsToRequest.request();
// 检查是否所有权限都已授予
bool allGranted = true;
// 6.用于记录被拒绝的权限(便于后续判断)
Permission? deniedPermission;
// 7.遍历权限状态集合,检查是否有任何权限被拒绝
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
deniedPermission = permission;
Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status');
}
});
// 单独检查麦克风权限状态,因为这是主要需求
final micStatus = await Permission.microphone.status;
// // 8. 如果是“永久拒绝”(用户勾选了“不再询问”)
// if (micStatus.isPermanentlyDenied) {
// _showPermissionDialog(true);
// return false;
// }
// 如果所有权限都已授予,或至少麦克风权限已授予,返回true
if (allGranted || micStatus.isGranted) {
return true;
}
// 处理特定权限被永久拒绝的情况
if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true);
return false;
}
// 普通拒绝情况
if (deniedPermission != null) {
_showPermissionDialog(false);
}
// // 9. 普通的拒绝情况,再次弹出说明对话框
// if (deniedPermission != null) {
// _showPermissionDialog(false);
// }
// 10. 如果权限未被授予,返回 false
return false;
} catch (e) {
Logger.e(TAG, '检查麦克风权限失败: $e');
@ -770,6 +780,87 @@ class AgentController extends GetxController {
}
}
// 显示权限提示对话框
void _showPermissionDialog(bool isPermanent) {
Get.generalDialog(
barrierDismissible: false,
barrierLabel: '权限提示',
transitionBuilder: (context, anim1, anim2, child) {
return SlideTransition(
position: Tween<Offset>(
begin: Offset(0, -1), // 从上方滑入
end: Offset(0, 0),
).animate(CurvedAnimation(parent: anim1, curve: Curves.easeOut)),
child: child,
);
},
pageBuilder: (context, anim1, anim2) {
return SafeArea(
child: Align(
alignment: Alignment.topCenter, // 关键:设置为顶部对齐
child: Padding(
padding: const EdgeInsets.only(top: 80, left: 24, right: 24),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('需要麦克风权限',
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Text(isPermanent
? '语音功能需要麦克风权限。请前往设置页面手动开启权限。'
: '语音功能需要麦克风权限。请允许使用麦克风权限以继续使用语音功能。'),
const SizedBox(height: 16),
const Text('权限使用说明:',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text('• 麦克风权限:用于录制您的语音,实现语音翻译、语音助手对话等核心功能'),
const SizedBox(height: 8),
const Text('• 我们仅在您主动使用语音功能时访问麦克风'),
const SizedBox(height: 8),
const Text('• 您可以随时在系统设置中关闭权限'),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消'),
),
const SizedBox(width: 8),
TextButton(
onPressed: () {
Get.back();
if (isPermanent) {
AppSettings.openAppSettings();
} else {
Permission.microphone.request();
}
},
child: Text(isPermanent ? '前往设置' : '授权'),
),
],
),
],
),
),
),
),
),
),
);
},
);
}
// 获取Android SDK版本
Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) {
@ -784,41 +875,20 @@ class AgentController extends GetxController {
return 0;
}
// 显示权限提示对话框
void _showPermissionDialog(bool isPermanent) {
Get.dialog(
AlertDialog(
title: const Text('需要麦克风权限'),
content: Text(isPermanent
? '语音功能需要麦克风权限。请前往设置页面手动开启权限。'
: '语音功能需要麦克风权限。请允许使用麦克风权限以继续使用语音功能。'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Get.back();
if (isPermanent) {
AppSettings.openAppSettings();
} else {
Permission.microphone.request();
}
},
child: Text(isPermanent ? '前往设置' : '授权'),
),
],
),
);
}
// 开始语音输入
Future<void> startVoiceInput() async {
if (isListening.value) return;
// 检查麦克风权限
final hasPermission = await _checkMicrophonePermission();
final hasPermission = await PermissionUtil.instance.requestPermission(
context: Get.context!,
permissionType: Permission.microphone,
permissionName: '麦克风',
explanationText: '语音识别与翻译功能需要访问麦克风。',
permanentDenialText: '您已永久拒绝麦克风权限,请前往系统设置开启权限。',
settingsButtonText: '去设置',
cancelButtonText: '取消',
);
if (!hasPermission) {
isTextInputMode.value = true; // 如果没有权限,切回文本模式
return;

103
lib/modules/agent/views/message_bubble.dart

@ -46,8 +46,6 @@ class MessageBubble extends GetView<AgentController> {
isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//if (!isUser) _buildAvatar(isUser, context),
//const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment:
@ -90,21 +88,41 @@ class MessageBubble extends GetView<AgentController> {
const SizedBox(height: 4),
// 识别中的消息不显示时间
if (!isRecognizing)
Text(
timeFormat.format(timestamp),
style: TextStyle(
fontSize: 10,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[400]
: Colors.grey[500],
fontWeight: FontWeight.w400,
),
Row(
mainAxisAlignment: isUser
? MainAxisAlignment.end
: MainAxisAlignment.spaceBetween,
children: [
// 时间戳(左对齐或右对齐)
Text(
timeFormat.format(timestamp),
style: TextStyle(
fontSize: 10,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[400]
: Colors.grey[500],
fontWeight: FontWeight.w400,
),
),
// AI标识(仅在AI消息时显示)
if (!isUser)
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
"由AI生成".tr,
style: TextStyle(
fontSize: 8,
color: Colors.blue[600],
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
),
),
),
],
),
],
),
),
//const SizedBox(width: 8),
//if (isUser) _buildAvatar(isUser, context),
],
),
);
@ -125,65 +143,6 @@ class MessageBubble extends GetView<AgentController> {
}
}
/// 构建头像
// Widget _buildAvatar(bool isUser, BuildContext context) {
// if (isUser) {
// final storage = GetStorage();
// final localAvatarPath = storage.read<String>('local_avatar') ?? '';
// return Container(
// width: 36,
// height: 36,
// decoration: BoxDecoration(
// color: localAvatarPath.isEmpty ? Colors.grey[300] : null,
// borderRadius: BorderRadius.circular(18),
// boxShadow: [
// BoxShadow(
// color: Colors.black.withOpacity(0.05),
// blurRadius: 2,
// offset: const Offset(0, 1),
// ),
// ],
// ),
// child: localAvatarPath.isNotEmpty
// ? ClipRRect(
// borderRadius: BorderRadius.circular(18),
// child: Image.file(
// File(localAvatarPath),
// fit: BoxFit.cover,
// ),
// )
// : Icon(
// Icons.person,
// color: Colors.grey[700],
// size: 20,
// ),
// );
// } else {
// // AI头像保持不变
// return Container(
// width: 36,
// height: 36,
// decoration: BoxDecoration(
// color: Theme.of(context).primaryColor,
// borderRadius: BorderRadius.circular(18),
// boxShadow: [
// BoxShadow(
// color: Colors.black.withOpacity(0.05),
// blurRadius: 2,
// offset: const Offset(0, 1),
// ),
// ],
// ),
// child: Icon(
// Icons.smart_toy_outlined,
// color: Colors.white,
// size: 20,
// ),
// );
// }
// }
/// 构建消息内容
Widget _buildMessageContent(BuildContext context) {
// 如果是识别中的消息,添加特殊样式

50
lib/modules/explore/controllers/explore_controller.dart

@ -75,28 +75,28 @@ class ExploreController extends GetxController {
agentId: 'simultaneous_interpreter',
targetRoute: Routes.translation,
),
AIFeature(
title: 'aiGirlfriend'.tr, // 亲子陪伴
description: 'aiGirlfriendInfo'.tr, // 智能陪伴,随时聊天
icon: Icons.favorite,
startColor: const Color(0xFFE91E63),
endColor: const Color(0xFFFF5252),
iconBgColor: const Color(0xFFE91E63).withOpacity(0.2),
iconColor: const Color(0xFFE91E63),
agentId: 'cyber_girlfriend',
targetRoute: Routes.chat,
),
AIFeature(
title: 'healthConsultant'.tr, // 健康顾问
description: 'healthConsultantInfo'.tr, // 提供科学健康建议
icon: Icons.healing,
startColor: const Color(0xFFFFC107),
endColor: const Color(0xFFFF9800),
iconBgColor: const Color(0xFFFFC107).withOpacity(0.2),
iconColor: const Color(0xFFFFC107),
agentId: 'health_advisor',
targetRoute: Routes.chat,
),
// AIFeature(
// title: 'aiGirlfriend'.tr, // 亲子陪伴
// description: 'aiGirlfriendInfo'.tr, // 智能陪伴,随时聊天
// icon: Icons.favorite,
// startColor: const Color(0xFFE91E63),
// endColor: const Color(0xFFFF5252),
// iconBgColor: const Color(0xFFE91E63).withOpacity(0.2),
// iconColor: const Color(0xFFE91E63),
// agentId: 'cyber_girlfriend',
// targetRoute: Routes.chat,
// ),
// AIFeature(
// title: 'healthConsultant'.tr, // 健康顾问
// description: 'healthConsultantInfo'.tr, // 提供科学健康建议
// icon: Icons.healing,
// startColor: const Color(0xFFFFC107),
// endColor: const Color(0xFFFF9800),
// iconBgColor: const Color(0xFFFFC107).withOpacity(0.2),
// iconColor: const Color(0xFFFFC107),
// agentId: 'health_advisor',
// targetRoute: Routes.chat,
// ),
];
// 热门功能场景列表
@ -105,9 +105,9 @@ class ExploreController extends GetxController {
title: 'conferenceAssistant'.tr, // 商务会议助手
description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译
icon: Icons.people,
startColor: const Color(0xFF3F51B5),
endColor: const Color(0xFF673AB7),
iconColor: const Color(0xFF3F51B5),
startColor: const Color(0xFFab47bc),
endColor: const Color(0xFFec407a),
iconColor: const Color(0xFFd81b60),
targetRoute: Routes.meeting,
),
FunctionScene(

20
lib/modules/home/views/home_view.dart

@ -226,7 +226,6 @@ class HomeView extends GetView<HomeController> {
// 设备信息
SizedBox(height: 12.h),
Obx(() {
// 如果连接失败且有错误信息,显示错误提示
if (controller.connectionStatus == ConnectionStatus.failed &&
controller.connectionErrorMsg.isNotEmpty) {
@ -861,23 +860,26 @@ class HomeView extends GetView<HomeController> {
SizedBox(height: 5.h), // 减少间距
// 右下卡片 - AI女友
// 右下卡片 -
// 右下卡片 - 会议助手
_buildFeatureCard(
title: 'aiCompanion'.tr, // 亲子陪伴
description: 'aiCompanionInfo'.tr, // 懂你心意,温暖陪伴
icon: Icons.favorite,
title: 'conferenceAssistant'.tr, // 商务会议助手
description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译
icon: Icons.people,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFE91E63).withOpacity(0.3),
Color(0xFFFF5252).withOpacity(0.3),
Color(0xFFab47bc).withOpacity(0.3), // 薰衣草紫
Color(0xFFec407a).withOpacity(0.3), // 品红
],
),
onTap: () {},
onTap: () {
Get.toNamed(Routes.meeting);
},
height: 85.h, // 增加高度
isDarkMode: isDarkMode,
iconColor: const Color(0xFFE91E63),
iconColor: const Color(0xFFd81b60), // 品红
),
],
),

66
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -40,7 +40,7 @@ class OtaUpgradeController extends GetxController {
final isUpdateAvailable = true.obs;
// 当前升级进度,范围为 0.0 到 1.0
final RxDouble progress = 0.0.obs;
// final RxDouble progress = 0.0.obs;
// OTA 单例实例,用于处理升级相关逻辑
final jlota = Ota.instance;
@ -52,7 +52,12 @@ class OtaUpgradeController extends GetxController {
final _bleManager = Get.find<BleManager>();
final maxRetryAttempts = 3; // 最大重试次数
int currentRetryCount = 0;
final RxBool isFetchingDeviceInfo = true.obs;
final RxDouble progress = 0.0.obs; // 控制器自己的可观察属性
final RxInt type = OtaEvent.NOT_STARTED.obs;
final RxBool isUpgrade = false.obs;
@override
void onInit() {
super.onInit();
@ -62,6 +67,7 @@ class OtaUpgradeController extends GetxController {
@override
void onClose() {
jlota.onOtaClose();
isUpgrade.value = false;
super.onClose();
}
@ -109,6 +115,7 @@ class OtaUpgradeController extends GetxController {
}
Future<void> _getCurrentVersion() async {
_listenToEvents();
try {
isFetchingDeviceInfo.value = true;
errorMessage.value = '';
@ -142,8 +149,20 @@ class OtaUpgradeController extends GetxController {
deviceInfo.firmwareProductId.isEmpty) {
throw "获取设备信息失败,请检查连接";
}
// 拆分 firmwareVersion 并判断两边是否一致
final firmwareParts = deviceInfo.firmwareVersion.split('.');
// 从第三个 `.` 开始分割
final firstPart = firmwareParts.sublist(0, 3).join('.');
final secondPart = firmwareParts.sublist(3).join('.');
print("firstPart${firstPart},secondPart=${secondPart}");
// 判断两部分是否一致
if (firstPart != secondPart) {
throw "固件版本格式错误:${deviceInfo.firmwareVersion}";
}
// 正常流程
currentVersion.value = deviceInfo.firmwareVersion;
currentVersion.value = firstPart;
print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}');
// API请求
final response = await Api.getequipmentinfo({
@ -256,7 +275,7 @@ class OtaUpgradeController extends GetxController {
headers: {'Accept-Encoding': 'gzip'},
));
jlota.type.value = DOWNLOADING_UP_FILES;
type.value = DOWNLOADING_UP_FILES;
try {
final Directory? downloadsDir = await getDownloadsDirectory();
if (downloadsDir == null) {
@ -352,4 +371,45 @@ class OtaUpgradeController extends GetxController {
return;
// isUpgrade.value = false;
}
// 停止升级
void stopUpgrading() async {
jlota.onOtaClose();
return;
// isUpgrade.value = false;
}
void _listenToEvents() {
Ota.instance.eventStream.listen((event) {
if (event.event == 'onProgress' &&
event.type != null &&
event.progress != null) {
progress.value = event.progress!;
type.value = event.type!;
isUpgrade.value = true; // 开始升级
}
if (event.event == 'onStartOTA') {
type.value = OtaEvent.START_UPGRADING;
isUpgrade.value = true; // 开始升级
}
if (event.event == 'onStopOTA') {
type.value = OtaEvent.UPGRADE_SUCCESS;
isUpgrade.value = false;
Get.snackbar('升级成功', '请退出重新连接设备');
_getCurrentVersion();
}
if (event.event == 'onError') {
type.value = OtaEvent.UPGRADE_FAILED;
isUpgrade.value = false;
}
if (event.event == 'NOCONNECTION') {
type.value = OtaEvent.CONNECTION_DISCONNECTION;
isUpgrade.value = false;
}
if (event.event == 'CONNECTION_SUCCESS') {
type.value = OtaEvent.CONNECTION_SUCCESS;
isUpgrade.value = true; // 开始升级
}
});
}
}

175
lib/modules/ota_upgrade/views/ota_upgrade_view.dart

@ -10,70 +10,125 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[100],
appBar: AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
elevation: 0,
backgroundColor: isDarkMode ? Colors.black : Colors.white,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.sp,
color: isDarkMode ? Colors.white : Colors.black,
return WillPopScope(
onWillPop: () async {
if (controller.isUpgrade.value) {
// 显示确认弹窗
await Get.dialog(
AlertDialog(
title: Text('提示'),
content: Text('当前正在升级,退出将中断升级进程,确定要退出吗?'),
actions: [
TextButton(
onPressed: () => Get.back(), // 关闭弹窗
child: Text('取消'),
),
TextButton(
onPressed: () {
controller.stopUpgrading();
Get.back(); // 关闭弹窗
Get.back(); // 返回上一页
},
child: Text('确定'),
),
],
),
);
return false; // 阻止默认返回
}
return true; // 允许返回
},
child: Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[100],
appBar: AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
elevation: 0,
backgroundColor: isDarkMode ? Colors.black : Colors.white,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
onPressed: () {
if (controller.isUpgrade.value) {
// 显示确认弹窗
Get.dialog(
AlertDialog(
title: Text('提示'),
content: Text('当前正在升级,退出将中断升级进程,确定要退出吗?'),
actions: [
TextButton(
onPressed: () => Get.back(), // 关闭弹窗
child: Text('取消'),
),
TextButton(
onPressed: () {
controller.stopUpgrading();
Get.back(); // 关闭弹窗
Get.back(); // 返回上一页
},
child: Text('确定'),
),
],
),
);
} else {
Get.back(); // 正常返回
}
},
),
onPressed: () => Get.back(),
),
title: Text(
'固件升级'.tr,
style: TextStyle(
fontSize: 20.sp,
color: isDarkMode ? Colors.white : Colors.black,
title: Text(
'固件升级'.tr,
style: TextStyle(
fontSize: 20.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 设备信息卡片
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: isDarkMode ? Colors.grey[900] : Colors.white,
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
_buildDeviceIcon(isDarkMode),
SizedBox(height: 20),
_buildVersionInfo(isDarkMode),
],
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 设备信息卡片
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: isDarkMode ? Colors.grey[900] : Colors.white,
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
_buildDeviceIcon(isDarkMode),
SizedBox(height: 20),
_buildVersionInfo(isDarkMode),
],
),
),
),
),
SizedBox(height: 20),
SizedBox(height: 20),
// 更新说明
_buildUpdateNotes(isDarkMode),
SizedBox(height: 20),
// 更新说明
_buildUpdateNotes(isDarkMode),
SizedBox(height: 20),
// 状态显示区域
Obx(() {
if (controller.isFetchingDeviceInfo.value) {
return _buildLoadingIndicator(isDarkMode);
}
if (controller.errorMessage.value.isNotEmpty) {
return _buildErrorInfo(isDarkMode);
}
return _buildUpdateButton(isDarkMode);
}),
],
// 状态显示区域
Obx(() {
if (controller.isFetchingDeviceInfo.value) {
return _buildLoadingIndicator(isDarkMode);
}
if (controller.errorMessage.value.isNotEmpty) {
return _buildErrorInfo(isDarkMode);
}
return _buildUpdateButton(isDarkMode);
}),
],
),
),
),
),
@ -227,7 +282,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
);
Widget _buildUpdateButton(bool isDarkMode) {
return controller.jlota.isUpgrade.value
return controller.isUpgrade.value
? _buildProgressIndicator(isDarkMode)
: Container(
width: double.infinity,
@ -276,8 +331,8 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
Widget _buildProgressIndicator(bool isDarkMode) {
return Obx(() {
final progress = controller.jlota.progress.value;
final type = controller.jlota.type.value;
final progress = controller.progress.value;
final type = controller.type.value;
// // 处理升级失败状态:弹出 snackbar 并显示错误提示
// if (type == OtaUpgradeController.UPGRADE_FAILED ||

92
lib/modules/pairing/controllers/pairing_controller.dart

@ -7,6 +7,7 @@ import '../../../core/utils/logger.dart';
import 'dart:io';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';
import '../../../core/utils/permission_util.dart';
/// 配对状态枚举
enum PairingState {
@ -178,13 +179,24 @@ class PairingController extends GetxController {
Logger.i(_tag, '开始执行初始化流程');
isLoading.value = true;
statusDescription.value = '正在检查权限...';
statusDescription.value = '正在初始化...';
try {
// 初始视觉反馈延迟
await Future.delayed(const Duration(milliseconds: 800));
// 更新状态让用户知道正在进行权限检查
statusDescription.value = '正在检查所需权限...';
// iOS平台跳过权限检查
if (!Platform.isIOS) {
// 添加额外视觉反馈延迟
await Future.delayed(const Duration(milliseconds: 600));
final hasPermission = await _checkAndRequestPermissions();
if (!hasPermission) {
statusDescription.value = '缺少必要的权限';
await Future.delayed(const Duration(milliseconds: 500));
_transition(PairingState.exitNoPermission);
return;
}
@ -193,6 +205,10 @@ class PairingController extends GetxController {
// 设置蓝牙状态监听
_setupBluetoothListener();
// 更新状态让用户知道正在进行蓝牙检查
statusDescription.value = '检查蓝牙状态...';
await Future.delayed(const Duration(milliseconds: 400));
// 检查蓝牙状态
final btState = BluetoothManager.to.bluetoothState;
Logger.i(_tag, '当前蓝牙状态: $btState');
@ -200,10 +216,14 @@ class PairingController extends GetxController {
if (btState == 'on' || btState == 'unknown') {
_transition(PairingState.btReady);
} else {
statusDescription.value = '蓝牙未开启';
await Future.delayed(const Duration(milliseconds: 500));
_transition(PairingState.exitBtOff);
}
} catch (e) {
Logger.e(_tag, 'Init failed: $e');
statusDescription.value = '初始化出错';
await Future.delayed(const Duration(milliseconds: 500));
_transition(PairingState.exitNoPermission);
} finally {
isLoading.value = false;
@ -359,7 +379,7 @@ class PairingController extends GetxController {
if (success) {
Logger.i(_tag, 'BLE配对启动成功,立即注册后台扫描');
// 配对启动成功后立即注册后台扫描
try {
// Android平台注册后台扫描
@ -372,7 +392,7 @@ class PairingController extends GetxController {
Logger.e(_tag, '注册后台扫描失败: $e');
// 注册失败不影响配对流程继续
}
Logger.i(_tag, '等待BLE连接完成...');
// 配对结果将通过BLE状态监听回调处理
} else {
@ -508,23 +528,75 @@ class PairingController extends GetxController {
return true;
}
// Android权限检查
final permissions = <Permission>[];
permissions.add(Permission.locationWhenInUse);
// Android权限检查 - 使用 PermissionUtil
final context = Get.context;
if (context == null) {
Logger.e(_tag, '无法获取有效上下文,跳过权限请求');
return false;
}
// 根据 Android 版本确定需要的权限
List<Permission> requiredPermissions = [
Permission.locationWhenInUse,
];
if (Platform.isAndroid && await _getAndroidSdkVersion() >= 31) {
permissions.add(Permission.bluetoothScan);
permissions.add(Permission.bluetoothConnect);
requiredPermissions.add(Permission.bluetoothScan);
requiredPermissions.add(Permission.bluetoothConnect);
}
// 逐个请求权限
for (var permission in requiredPermissions) {
final permissionName = _getPermissionName(permission);
Logger.d(_tag, '正在请求 $permissionName 权限');
final granted = await PermissionUtil.instance.requestPermission(
context: context,
permissionType: permission,
permissionName: permissionName,
explanationText: _getExplanationText(permission),
permanentDenialText: '您已永久拒绝$permissionName权限,请前往应用设置开启',
settingsButtonText: '去设置',
cancelButtonText: '取消',
);
if (!granted) {
Logger.w(_tag, '$permissionName 权限未授予');
return false;
}
}
final statuses = await permissions.request();
return statuses.values.every((status) => status.isGranted);
return true;
} catch (e) {
Logger.e(_tag, '权限检查失败: $e');
return false;
}
}
// 获取权限的用户友好名称
String _getPermissionName(Permission permission) {
if (permission == Permission.location) return '定位';
if (permission == Permission.locationWhenInUse) return '定位';
if (permission == Permission.bluetoothScan) return '蓝牙扫描';
if (permission == Permission.bluetoothConnect) return '蓝牙连接';
return '未知权限';
}
// 获取权限说明文本
String _getExplanationText(Permission permission) {
if (permission == Permission.location ||
permission == Permission.locationWhenInUse) {
return '用于发现和连接蓝牙设备。我们不会在应用后台使用定位功能。';
}
if (permission == Permission.bluetoothScan ||
permission == Permission.bluetoothConnect) {
return '用于扫描和连接蓝牙设备,这是与耳机通信所必需的权限。';
}
return '此权限是应用正常运行所必需的。';
}
/// 设置蓝牙状态监听
void _setupBluetoothListener() {
_bluetoothStateSubscription?.cancel();

225
lib/modules/settings/views/permissions_view.dart

@ -4,6 +4,7 @@ import 'package:permission_handler/permission_handler.dart';
import '../controllers/settings_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui';
import 'package:deep_voice/core/utils/permission_util.dart'; // 引入PermissionUtil
class PermissionsView extends GetView<SettingsController> {
const PermissionsView({Key? key}) : super(key: key);
@ -11,7 +12,7 @@ class PermissionsView extends GetView<SettingsController> {
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
@ -41,45 +42,53 @@ class PermissionsView extends GetView<SettingsController> {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('获取权限状态失败: ${snapshot.error}'.tr));
}
final permissionStatuses = snapshot.data ?? {};
return ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: [
// 通讯权限组
_buildPermissionSection('通讯权限', Icons.phone_android, [
Permission.phone,
Permission.sms
], permissionStatuses, isDarkMode),
_buildPermissionSection(
'通讯权限',
Icons.phone_android,
[Permission.phone, Permission.sms],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h),
// 日历权限组
_buildPermissionSection('日历权限', Icons.calendar_today, [
Permission.calendarFullAccess
], permissionStatuses, isDarkMode),
_buildPermissionSection(
'日历权限',
Icons.calendar_today,
[Permission.calendarFullAccess],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h),
// 位置权限组
_buildPermissionSection('位置权限', Icons.location_on, [
Permission.location
], permissionStatuses, isDarkMode),
_buildPermissionSection('位置权限', Icons.location_on,
[Permission.location], permissionStatuses, isDarkMode),
],
);
},
),
);
}
// 构建权限组区域
Widget _buildPermissionSection(String title, IconData icon, List<Permission> permissions,
Map<Permission, PermissionStatus> statuses, bool isDarkMode) {
Widget _buildPermissionSection(
String title,
IconData icon,
List<Permission> permissions,
Map<Permission, PermissionStatus> statuses,
bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -96,7 +105,7 @@ class PermissionsView extends GetView<SettingsController> {
),
),
),
// 权限项容器
Container(
decoration: BoxDecoration(
@ -119,7 +128,8 @@ class PermissionsView extends GetView<SettingsController> {
? ImageFilter.blur(sigmaX: 5, sigmaY: 5)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Column(
children: _buildPermissionItems(permissions, statuses, isDarkMode),
children:
_buildPermissionItems(permissions, statuses, isDarkMode),
),
),
),
@ -127,52 +137,52 @@ class PermissionsView extends GetView<SettingsController> {
],
);
}
// 构建权限项列表
List<Widget> _buildPermissionItems(List<Permission> permissions,
List<Widget> _buildPermissionItems(List<Permission> permissions,
Map<Permission, PermissionStatus> statuses, bool isDarkMode) {
final List<Widget> items = [];
// 获取描述文本
String getDescription(Permission permission) {
// 获取名称
String getName(Permission permission) {
switch (permission) {
case Permission.phone:
return '用于拨打电话功能';
return '电话';
case Permission.sms:
return '用于发送短信功能';
return '短信';
case Permission.calendarWriteOnly:
return '用于创建日历事件和提醒';
return '日历写入';
case Permission.calendarFullAccess:
return '用于读取和管理日历事件及提醒';
return '日历完整访问';
case Permission.location:
return '用于获取当前位置信息';
return '位置信息';
default:
return '应用功能需要';
return permission.toString();
}
}
// 获取名称
String getName(Permission permission) {
// 获取描述文本
String getDescription(Permission permission) {
switch (permission) {
case Permission.phone:
return '电话';
return '用于拨打电话功能';
case Permission.sms:
return '短信';
return '用于发送短信功能';
case Permission.calendarWriteOnly:
return '日历写入';
return '用于创建日历事件和提醒';
case Permission.calendarFullAccess:
return '日历完整访问';
return '用于读取和管理日历事件及提醒';
case Permission.location:
return '位置信息';
return '用于获取当前位置信息';
default:
return permission.toString();
return '应用功能需要';
}
}
for (int i = 0; i < permissions.length; i++) {
final permission = permissions[i];
final status = statuses[permission] ?? PermissionStatus.denied;
items.add(
_buildPermissionItem(
name: getName(permission),
@ -182,7 +192,7 @@ class PermissionsView extends GetView<SettingsController> {
isDarkMode: isDarkMode,
),
);
// 添加分隔线,最后一个不加
if (i < permissions.length - 1) {
items.add(Divider(
@ -191,10 +201,10 @@ class PermissionsView extends GetView<SettingsController> {
));
}
}
return items;
}
// 构建单个权限项
Widget _buildPermissionItem({
required String name,
@ -248,12 +258,12 @@ class PermissionsView extends GetView<SettingsController> {
),
);
}
// 状态文本
Widget _buildStatusText(PermissionStatus status, bool isDarkMode) {
String text;
Color color;
if (status.isGranted) {
text = '已授权'.tr;
color = Colors.green;
@ -262,7 +272,7 @@ class PermissionsView extends GetView<SettingsController> {
color = Colors.red;
} else if (status.isPermanentlyDenied) {
text = '需要在系统设置中开启'.tr;
color = Colors.grey;
color = Colors.orange;
} else if (status.isLimited) {
text = '部分授权'.tr;
color = Colors.orange;
@ -270,7 +280,7 @@ class PermissionsView extends GetView<SettingsController> {
text = '未知状态'.tr;
color = Colors.grey;
}
return Text(
text,
style: TextStyle(
@ -280,45 +290,84 @@ class PermissionsView extends GetView<SettingsController> {
),
);
}
// 处理权限请求
Future<void> _handlePermissionAction(Permission permission, PermissionStatus status) async {
if (status.isPermanentlyDenied) {
// 打开应用设置
controller.openSettings();
return;
}
if (!status.isGranted) {
// 请求权限
final result = await controller.requestPermission(permission);
if (result.isGranted) {
Get.snackbar('成功'.tr, '权限已授予'.tr, snackPosition: SnackPosition.BOTTOM);
} else if (result.isPermanentlyDenied) {
Get.dialog(
AlertDialog(
title: Text('需要权限'.tr),
content: Text('此权限需要在设置中手动开启'.tr),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('取消'.tr),
),
TextButton(
onPressed: () {
Get.back();
controller.openSettings();
},
child: Text('去设置'.tr),
),
],
),
Future<void> _handlePermissionAction(
Permission permission, PermissionStatus status) async {
try {
final context = Get.context;
if (context == null) return;
final permissionName = _getPermissionName(permission);
final explanationText = _getExplanationText(permission);
// 使用 PermissionUtil 请求权限
final hasPermission = await PermissionUtil.instance.requestPermission(
context: context,
permissionType: permission,
permissionName: permissionName,
explanationText: explanationText,
permanentDenialText: '您已永久拒绝$permissionName权限,请前往应用设置开启',
settingsButtonText: '去设置',
cancelButtonText: '取消',
);
if (hasPermission) {
Get.snackbar(
'成功'.tr,
'$permissionName权限已授予'.tr,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
}
// 刷新界面
// 刷新界面,重新获取权限状态
Get.forceAppUpdate();
} catch (e) {
Get.snackbar(
'错误'.tr,
'请求权限时出错: $e'.tr,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// 获取权限的用户友好名称
String _getPermissionName(Permission permission) {
switch (permission) {
case Permission.phone:
return '电话';
case Permission.sms:
return '短信';
case Permission.calendarWriteOnly:
return '日历写入';
case Permission.calendarFullAccess:
return '日历完整访问';
case Permission.location:
return '位置信息';
default:
return permission.toString();
}
}
// 获取权限说明文本
String _getExplanationText(Permission permission) {
switch (permission) {
case Permission.phone:
return '拨打电话功能需要电话权限';
case Permission.sms:
return '发送短信功能需要短信权限';
case Permission.calendarWriteOnly:
return '创建日历事件需要日历写入权限';
case Permission.calendarFullAccess:
return '管理日历事件需要完整日历权限';
case Permission.location:
return '获取位置信息需要位置权限';
default:
return '应用需要此权限以提供完整功能';
}
}
}
}

20
lib/modules/translation/controllers/translation_controller.dart

@ -33,7 +33,7 @@ class TranslationController extends GetxController {
final isRecognizing = false.obs;
final isTranslating = false.obs;
final isTtsEnabled = true.obs;
final isRecording = false.obs;
// 语言相关
final sourceLanguage = '中文(简体)'.obs;
final targetLanguage = '英语'.obs;
@ -193,6 +193,16 @@ class TranslationController extends GetxController {
// 切换TTS功能
void toggleTts() => isTtsEnabled.toggle();
// 切换录音功能
Future<void> toggleisRecord() async {
isRecording.toggle();
if (isRecording.value) {
await _asrService.enableRecord();
} else {
await _asrService.stopRecord();
}
}
// 开始语音识别
Future<void> startRecognition() async {
Logger.info('开始语音识别');
@ -233,15 +243,18 @@ class TranslationController extends GetxController {
_bleManager.openDecoder();
// 发送ble音乐或者通话远端声音
_audioSourceType = true;
isTtsEnabled.value = false; // 禁用TTS以避免干扰
Logger.info('发送ble音乐或者通话远端声音');
} else if (currentMode.value == "call") {
// 发送ble系统mic和dac(音乐或者通话远端)声音
_bleManager.openA2DPDecoder();
_audioSourceType = true;
isTtsEnabled.value = true;
Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音');
} else {
// 开始连续语音识别
_audioSourceType = false;
isTtsEnabled.value = true;
}
final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType);
@ -476,6 +489,11 @@ class TranslationController extends GetxController {
// 停止所有操作
Future<void> stopAll() async {
try {
if (currentMode.value == "audioVideo" || currentMode.value == "call") {
_bleManager.closeCodec();
// 发送ble音乐或者通话远端声音
Logger.info('发送ble音乐或者通话远端声音');
}
if (isRecognizing.value) {
await stopRecognition();
}

6
lib/modules/translation/views/translation_view.dart

@ -87,13 +87,13 @@ class TranslationView extends GetView<TranslationController> {
// TTS toggle button
Obx(() => IconButton(
icon: Icon(
controller.isTtsEnabled.value ? Icons.mic : Icons.mic_off,
controller.isRecording.value ? Icons.mic : Icons.mic_off,
color: isDarkMode ? Colors.white : Colors.black,
size: 24.sp,
),
// onPressed: controller.toggleTts,默认播放
onPressed: controller.toggleTts,
tooltip: '自动播放翻译结果',
onPressed: controller.toggleisRecord,
tooltip: '是否开启录音',
)),
// 历史记录按钮

2
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -1,8 +1,6 @@
package com.yunqiinnovation.agent_service
import android.content.Context
import com.yunqiinnovation.agent_service.AgentService
import com.yunqiinnovation.agent_service.AgentServiceListener
import android.util.Log
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst

1
local_plugins/azure_speech/android/build.gradle.kts

@ -54,5 +54,6 @@ dependencies {
// 添加Microsoft语音SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.43.0")
implementation(project(":speech"))
add("compileOnly", project(":ble_service"))
}

370
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -28,51 +28,56 @@ import java.util.concurrent.TimeUnit
import java.io.File
import java.io.FileOutputStream
import java.io.RandomAccessFile
/**
* Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别
*
*
* 针对回音消除的场景,使用拉流模式 (PullAudioInputStreamCallback) 实现,避免复杂的协程处理
* 通过检测扬声器和耳机状态,自动决定是否启用回音消除
*/
class AzureAsrHelper(private val context: Context) {
private val tag = "AzureAsrHelper"
// 核心组件
private var speechConfig: SpeechConfig? = null
private var recognizer: SpeechRecognizer? = null
private var audioConfig: AudioConfig? = null
// 状态管理
private var isContinuousRecognitionActive = false
// 配置参数
private var currentLanguage = "zh-CN"
private var supportedLanguages = arrayOf("zh-CN")
private var isAutoDetectLanguage = false
private var subscriptionKey = ""
private var region = ""
// 音频源配置
private var audioSourceType = AudioSourceType.MICROPHONE
var audioSourceType = AudioSourceType.MICROPHONE
// 音频处理
private var microphoneStream: MicrophoneStream? = null
private var externalAudioStream: ExternalAudioPullStream? = null
// 录音文件处理
private var recordfile: RecordingFile? = null
private var isRecording = false
/**
* 音频来源类型
*/
enum class AudioSourceType {
/** 使用设备麦克风 */
MICROPHONE,
/** 使用外部提供的音频数据 */
EXTERNAL
}
/**
* 初始化Azure语音服务
*
*
* @param subscriptionKey Azure 订阅密钥
* @param region Azure 区域
* @param supportedLanguages 支持的语言数组
@ -91,28 +96,28 @@ class AzureAsrHelper(private val context: Context) {
Log.e(tag, "Azure 配置信息不完整")
return false
}
// 释放之前的资源
dispose()
// 保存配置
this.subscriptionKey = subscriptionKey
this.region = region
this.audioSourceType = audioSourceType
// 设置语言
if (supportedLanguages.isNotEmpty()) {
this.supportedLanguages = supportedLanguages
}
// 根据支持的语言数量决定是否启用自动语言检测
this.isAutoDetectLanguage = supportedLanguages.size >= 2
// 如果只有一种语言,设置为当前语言
if (!isAutoDetectLanguage && supportedLanguages.isNotEmpty()) {
this.currentLanguage = supportedLanguages[0]
}
// 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region).apply {
if (isAutoDetectLanguage) {
@ -135,7 +140,8 @@ class AzureAsrHelper(private val context: Context) {
// 设置分段策略为时间模式
setProperty("Speech_SegmentationStrategy", "Time")
}
// 录音文件类
recordfile = RecordingFile()
// 创建识别器
val setupSuccess = setupRecognizer()
if (setupSuccess) {
@ -148,7 +154,7 @@ class AzureAsrHelper(private val context: Context) {
return false
}
}
/**
* 设置识别器
*/
@ -157,17 +163,18 @@ class AzureAsrHelper(private val context: Context) {
// 清理旧的识别器
recognizer?.close()
recognizer = null
// 设置音频配置
when (audioSourceType) {
AudioSourceType.MICROPHONE -> {
// 判断是否使用手机扬声器输出和耳机连接状态
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val audioManager =
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val isSpeakerphoneOn = audioManager.isSpeakerphoneOn
// 检查是否连接了有线耳机
val isWiredHeadsetOn = audioManager.isWiredHeadsetOn
// 检查是否连接了蓝牙设备(需要注意兼容性)
val isBluetoothConnected = try {
val method = AudioManager::class.java.getMethod("isBluetoothA2dpOn")
@ -175,12 +182,12 @@ class AzureAsrHelper(private val context: Context) {
} catch (e: Exception) {
false
}
val isHeadsetConnected = isWiredHeadsetOn || isBluetoothConnected
// 如果使用手机扬声器输出且没有连接耳机,可能会产生回音,需启动回音消除功能
val needEchoCancellation = !isHeadsetConnected
if (needEchoCancellation) {
// 使用拉流方式进行回音消除
setupMicrophoneStream()
@ -188,20 +195,22 @@ class AzureAsrHelper(private val context: Context) {
audioConfig = AudioConfig.fromDefaultMicrophoneInput()
}
}
AudioSourceType.EXTERNAL -> {
// 改用拉流方式处理外部音频
setupExternalAudioStream()
}
}
// 创建识别器
recognizer = if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
val autoDetectConfig =
AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig)
} else {
SpeechRecognizer(speechConfig, audioConfig)
}
return true
} catch (e: Exception) {
Log.e(tag, "创建识别器失败: ${e.message}")
@ -209,7 +218,7 @@ class AzureAsrHelper(private val context: Context) {
return false
}
}
/**
* 设置麦克风流 - 使用拉流方式
*/
@ -217,7 +226,7 @@ class AzureAsrHelper(private val context: Context) {
try {
// 创建麦克风流
microphoneStream = MicrophoneStream()
// 创建音频配置 - 正确使用fromStreamInput方法,只传入回调
audioConfig = AudioConfig.fromStreamInput(microphoneStream)
} catch (e: Exception) {
@ -263,7 +272,7 @@ class AzureAsrHelper(private val context: Context) {
try {
// 创建外部音频拉流对象
externalAudioStream = ExternalAudioPullStream()
// 创建音频配置
audioConfig = AudioConfig.fromStreamInput(externalAudioStream)
} catch (e: Exception) {
@ -271,56 +280,61 @@ class AzureAsrHelper(private val context: Context) {
externalAudioStream = null
}
}
/**
* 向音频流写入音频数据
* 仅当音频源设置为EXTERNAL时有效
*
*
* @param data 音频数据字节数组
*/
fun pushAudioData(data: ByteArray) {
if (audioSourceType != AudioSourceType.EXTERNAL) {
return
}
// 使用拉流模式,将数据推入队列
externalAudioStream?.pushAudio(data)
}
/**
* 执行一次性语音识别
*
*
* @param callback 识别结果回调
*/
fun recognizeOnce(callback: RecognizeCallback, audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE) {
fun recognizeOnce(
callback: RecognizeCallback,
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE
) {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return
}
this.audioSourceType = audioSourceType
this.audioSourceType = audioSourceType
// 确保不在连续识别中
if (isContinuousRecognitionActive) {
stopContinuousRecognition()
}
// 重置识别器
if (!setupRecognizer()) {
callback.onError("重置识别器失败")
return
}
try {
// 启动音频处理
startAudioProcessing()
// 执行同步识别
val result = recognizer?.recognizeOnceAsync()?.get()
// 停止音频处理
stopAudioProcessing()
if (result != null && result.reason == ResultReason.RecognizedSpeech) {
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language ?: supportedLanguages[0]
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language
?: supportedLanguages[0]
callback.onResult(result.text, detectedLanguage)
} else {
callback.onError("未能识别语音")
@ -330,19 +344,22 @@ class AzureAsrHelper(private val context: Context) {
callback.onError("识别异常: ${e.message}")
}
}
/**
* 开始连续语音识别
*
*
* @param callback 连续识别结果回调
* @return 是否成功开始识别
*/
fun startContinuousRecognition(callback: ContinuousRecognizeCallback, audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE): Boolean {
fun startContinuousRecognition(
callback: ContinuousRecognizeCallback,
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE
): Boolean {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return false
}
if (isContinuousRecognitionActive) {
return true
}
@ -352,18 +369,21 @@ class AzureAsrHelper(private val context: Context) {
callback.onError("重置识别器失败")
return false
}
try {
// 设置各种事件监听
setupEventListeners(callback)
// 启动音频处理
startAudioProcessing()
// 开始连续识别
recognizer?.startContinuousRecognitionAsync()
isContinuousRecognitionActive = true
Log.e(tag, "开始连续语音识别 是否录音${isRecording}")
if (isRecording) {
enableRecord()
}
return true
} catch (e: Exception) {
stopAudioProcessing()
@ -372,7 +392,7 @@ class AzureAsrHelper(private val context: Context) {
return false
}
}
/**
* 设置事件监听器
*/
@ -434,7 +454,7 @@ class AzureAsrHelper(private val context: Context) {
)
}
/**
* 停止连续语音识别
*
@ -445,11 +465,11 @@ class AzureAsrHelper(private val context: Context) {
Log.e(tag, "语音服务未初始化")
return false
}
if (!isContinuousRecognitionActive) {
return true
}
try {
if (recognizer == null) {
Log.w(tag, "识别器为空,重置状态")
@ -459,23 +479,24 @@ class AzureAsrHelper(private val context: Context) {
if (audioSourceType == AudioSourceType.EXTERNAL) {
pushAudioData(ByteArray(0))
}
// 直接停止连续识别(SDK内部已是异步操作)
recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS)
// 停止音频处理
stopAudioProcessing()
// 停止录音
recordfile!!.closeFile()
// 会话结束事件会设置isContinuousRecognitionActive = false
return true
} catch (e: Exception) {
// 强制重置状态
isContinuousRecognitionActive = false
Log.e(tag, "停止连续识别失败: ${e.message}")
// 停止音频处理
stopAudioProcessing()
recordfile!!.closeFile()
// 尝试强制关闭识别器
try {
recognizer?.close()
@ -483,16 +504,16 @@ class AzureAsrHelper(private val context: Context) {
} catch (ex: Exception) {
Log.e(tag, "关闭识别器失败: ${ex.message}")
}
return false
}
}
/**
* 检查连续识别是否活跃
*/
fun isContinuousRecognitionActive(): Boolean = isContinuousRecognitionActive
/**
* 释放所有资源
*/
@ -504,22 +525,23 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false
}
// 停止音频处理
stopAudioProcessing()
// 停止录音
recordfile!!.closeFile()
// 释放recognizer
recognizer?.close()
recognizer = null
// 释放speechConfig
speechConfig?.close()
speechConfig = null
// 释放音频配置
audioConfig?.close()
audioConfig = null
// 确保状态被重置
isContinuousRecognitionActive = false
microphoneStream = null
@ -537,9 +559,9 @@ class AzureAsrHelper(private val context: Context) {
speechConfig = null
}
}
// 音频处理相关方法
/**
* 启动音频处理
*/
@ -549,12 +571,13 @@ class AzureAsrHelper(private val context: Context) {
// 拉流模式不需要额外启动,SDK会自动拉取数据
// 无需执行任何操作
}
AudioSourceType.EXTERNAL -> {
// 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData
}
}
}
/**
* 停止音频处理
*/
@ -568,7 +591,7 @@ class AzureAsrHelper(private val context: Context) {
e.printStackTrace()
}
}
externalAudioStream?.let {
try {
it.close()
@ -579,7 +602,31 @@ class AzureAsrHelper(private val context: Context) {
}
}
}
/**
* 开启录音
*/
fun enableRecord() {
Log.i(tag, "开启录音:")
isRecording = true
recordfile!!.closeFile()
if (isContinuousRecognitionActive) {
recordfile!!.creatingFiles()
}
}
/**
* 停止录音
*/
fun stopRecord() {
Log.i(tag, "停止连续录音:")
isRecording = false
recordfile!!.closeFile()
}
/**
* 麦克风流 - 拉流模式
* 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据
@ -599,7 +646,7 @@ class AzureAsrHelper(private val context: Context) {
init {
initMicrophone()
}
/**
* 初始化麦克风
*/
@ -612,7 +659,7 @@ class AzureAsrHelper(private val context: Context) {
.setEncoding(audioFormat)
.setChannelMask(channelConfig)
.build()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(format)
@ -627,18 +674,18 @@ class AzureAsrHelper(private val context: Context) {
bufferSize
)
}
// 检查初始化状态
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
throw IllegalStateException("AudioRecord初始化失败")
}
// 应用音频效果
applyAudioEffects()
// 开始录音
audioRecord?.startRecording()
// 检查录音状态
if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
throw IllegalStateException("录音启动失败")
@ -647,7 +694,7 @@ class AzureAsrHelper(private val context: Context) {
e.printStackTrace()
}
}
/**
* 优化:条件性应用音频效果
*/
@ -664,12 +711,13 @@ class AzureAsrHelper(private val context: Context) {
Log.d(tag, "跳过回音消除(使用耳机)")
}
}
/**
* 获取音频格式
*/
fun getFormat(): AudioStreamFormat = AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort())
fun getFormat(): AudioStreamFormat =
AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort())
/**
* Azure SDK 调用此方法获取音频数据
* 这是拉流模式的核心方法,由SDK调用以获取音频数据
@ -697,7 +745,7 @@ class AzureAsrHelper(private val context: Context) {
override fun close() {
releaseAudioResources()
}
/**
* 释放音频资源
*/
@ -707,11 +755,11 @@ class AzureAsrHelper(private val context: Context) {
if (audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
audioRecord?.stop()
}
// 释放回音消除器
echoCanceler?.release()
echoCanceler = null
// 释放录音实例
audioRecord?.release()
audioRecord = null
@ -720,7 +768,7 @@ class AzureAsrHelper(private val context: Context) {
}
}
}
/**
* 外部音频拉流
* 实现PullAudioInputStreamCallback,将外部推送的音频数据转换为SDK可拉取的形式
@ -728,17 +776,19 @@ class AzureAsrHelper(private val context: Context) {
private inner class ExternalAudioPullStream : PullAudioInputStreamCallback() {
private val queue: BlockingQueue<ByteArray> = LinkedBlockingQueue()
private var closed = false
/**
* 外部调用:推送音频数据到队列
* @param data 音频数据
*/
fun pushAudio(data: ByteArray) {
// 将音频数据保存成wav格式的音频文件
recordfile?.saveAudioDataToWav(data)
if (!closed) {
queue.offer(data)
}
}
/**
* 修复:外部音频流恢复阻塞读取
* @param buffer SDK提供的缓冲区
@ -756,6 +806,7 @@ class AzureAsrHelper(private val context: Context) {
val toCopy = minOf(chunk.size, buffer.size)
System.arraycopy(chunk, 0, buffer, 0, toCopy)
Log.i(tag, "外部音频:")
return toCopy
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
@ -765,7 +816,7 @@ class AzureAsrHelper(private val context: Context) {
return 0
}
}
/**
* SDK调用:关闭流
*/
@ -774,70 +825,177 @@ class AzureAsrHelper(private val context: Context) {
queue.clear()
}
}
/**
* 一次性识别回调接口
*/
interface RecognizeCallback {
/**
* 返回识别结果
*
*
* @param text 识别的文本
* @param detectedLanguage 检测到的语言
*/
fun onResult(text: String, detectedLanguage: String)
/**
* 识别错误时调用
*
*
* @param error 错误信息
*/
fun onError(error: String)
}
/**
* 连续识别回调接口
*/
interface ContinuousRecognizeCallback {
/**
* 返回识别结果
*
*
* @param text 识别的文本
* @param detectedLanguage 检测到的语言
*/
fun onResult(text: String, detectedLanguage: String)
/**
* 识别进行中调用
*
*
* @param recognizing 正在识别的文本
* @param detectedLanguage 检测到的语言
*/
fun onRecognizing(recognizing: String, detectedLanguage: String)
/**
* 会话开始时调用
*/
fun onSessionStarted()
/**
* 会话结束时调用
*/
fun onSessionStopped()
/**
* 识别取消时调用
*
*
* @param reason 取消原因
* @param errorDetails 错误详情
*/
fun onCanceled(reason: String, errorDetails: String)
/**
* 识别出错时调用
*
*
* @param error 错误信息
*/
fun onError(error: String)
}
private inner class RecordingFile {
private var currentAudioFile: File? = null
private var fos: FileOutputStream? = null
// 用于存储音频数据的缓冲区
private val dataBuffer = mutableListOf<ByteArray>()
private var totalBytesWritten = 0
// 音频配置
private val sampleRate = 16000
internal fun creatingFiles() {
if (fos != null || currentAudioFile != null) {
return
}
// 创建新的音频文件
val timestamp = System.currentTimeMillis()
val filePath =
context.getExternalFilesDir(null)?.absolutePath + "/recorded_audio_$timestamp.wav"
currentAudioFile = File(filePath)
currentAudioFile?.createNewFile()
// 仅写入初始文件头
fos = FileOutputStream(currentAudioFile).apply {
write(generateWavHeader(0)) // 初始0长度
close()
}
// 追加音频数据到文件
fos = FileOutputStream(currentAudioFile, true)
}
/**
* 保存音频数据到 WAV 文件
*/
internal fun saveAudioDataToWav(buffer: ByteArray) {
if (fos == null || currentAudioFile == null) {
return
}
// 复制数据到新数组
val copy = ByteArray(buffer.size)
System.arraycopy(buffer, 0, copy, 0, buffer.size)
// 添加到缓冲区
dataBuffer.add(copy)
totalBytesWritten += buffer.size
// 写入文件
try {
fos?.write(copy, 0, buffer.size)
} catch (e: Exception) {
Log.e(tag, "写入音频数据失败: ${e.message}")
}
}
// 更新文件头生成(修正RIFF长度计算)
private fun generateWavHeader(dataLength: Int): ByteArray {
val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据
val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数
return byteArrayOf(
'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(),
(totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(),
((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(),
'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(),
'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(),
16, 0, 0, 0, // PCM头长度
1, 0, // PCM格式
1, 0, // 单声道
(sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), 0, 0, // 采样率
(byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), 0, 0, // 字节率
2, 0, // 块对齐 (通道数 * 样本位数/8)
16, 0, // 样本位数
'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(),
(dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(),
((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte()
)
}
internal fun closeFile() {
try {
if (fos == null || currentAudioFile == null) {
return
}
// 关闭文件流
fos?.close()
// 更新WAV文件头
currentAudioFile?.let { file ->
RandomAccessFile(file, "rw").use { raf ->
raf.seek(0)
raf.write(generateWavHeader(totalBytesWritten))
}
Log.d(tag, "音频文件保存完成: ${file.absolutePath}")
}
} catch (e: Exception) {
Log.e(tag, "更新WAV文件头失败: ${e.message}")
} finally {
fos = null
currentAudioFile = null
}
}
}
}

833
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -15,384 +15,499 @@ import com.microsoft.cognitiveservices.speech.audio.*
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.yunqiinnovation.ble_service.BleService
/** AzureSpeechPlugin */
class AzureSpeechPlugin: FlutterPlugin {
private val tag = "AzureSpeechPlugin"
private lateinit var context: Context
private val mainHandler = Handler(Looper.getMainLooper())
// ASR相关
private lateinit var asrChannel : MethodChannel
private lateinit var asrEventChannel: EventChannel
private var asrEventSink: EventChannel.EventSink? = null
private lateinit var azureAsrHelper: AzureAsrHelper
// TTS相关
private lateinit var ttsChannel : MethodChannel
private lateinit var ttsEventChannel: EventChannel
private var ttsEventSink: EventChannel.EventSink? = null
private lateinit var azureTtsHelper: AzureTtsHelper
// 是否已添加TTS事件监听器
private var isTtsListenerAdded = false
// ASR 事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) {
if (asrEventSink == null) {
FileLogger.w(tag, "无法发送ASR事件:事件通道未准备好")
return
}
class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
private val tag = "AzureSpeechPlugin"
private lateinit var context: Context
private val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post {
try {
asrEventSink?.success(event)
} catch (e: Exception) {
FileLogger.e(tag, "发送ASR事件失败: ${e.message}")
}
}
}
// TTS 事件发送方法
private fun sendTtsEvent(event: Map<String, Any>) {
if (ttsEventSink == null) {
FileLogger.w(tag, "无法发送TTS事件:事件通道未准备好")
return
}
// 确保在主线程上发送事件
mainHandler.post {
try {
ttsEventSink?.success(event)
} catch (e: Exception) {
FileLogger.e(tag, "发送TTS事件失败: ${e.message}")
}
}
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
// 初始化ASR通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr")
asrChannel.setMethodCallHandler(AsrMethodHandler())
// 初始化TTS通道
ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/tts")
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化ASR事件通道
asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr_events")
asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
asrEventSink = events
}
override fun onCancel(arguments: Any?) {
asrEventSink = null
}
})
// 初始化TTS事件通道
ttsEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/tts_events")
ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events
setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器
}
override fun onCancel(arguments: Any?) {
ttsEventSink = null
}
})
// 初始化Azure语音服务
azureTtsHelper = AzureTtsHelper(context)
azureAsrHelper = AzureAsrHelper(context)
}
/**
* 设置TTS事件监听器
*/
private fun setupTtsEventListener() {
if (!isTtsListenerAdded) {
// 添加TTS事件监听器
azureTtsHelper.addListener(object : TtsEventListener {
override fun onEvent(event: TtsEvent) {
// 将TTS事件转换为Flutter可理解的格式
val eventMap = when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> mapOf(
"type" to "synthesis_started"
)
TtsEventType.SYNTHESIS_COMPLETED -> mapOf(
"type" to "synthesis_completed"
)
TtsEventType.SYNTHESIS_CANCELED -> {
val params = event.params
mapOf(
"type" to "synthesis_canceled",
"reason" to (params["reason"] ?: ""),
"errorDetails" to (params["errorDetails"] ?: "")
)
}
TtsEventType.ERROR -> {
val params = event.params
mapOf(
"type" to "error",
"errorCode" to (params["errorCode"] ?: "UNKNOWN_ERROR"),
"errorMessage" to (params["errorMessage"] ?: "未知错误")
)
// ASR相关
private lateinit var asrChannel: MethodChannel
private lateinit var asrEventChannel: EventChannel
private var asrEventSink: EventChannel.EventSink? = null
private lateinit var azureAsrHelper: AzureAsrHelper
// TTS相关
private lateinit var ttsChannel: MethodChannel
private lateinit var ttsEventChannel: EventChannel
private var ttsEventSink: EventChannel.EventSink? = null
private lateinit var azureTtsHelper: AzureTtsHelper
// 是否已添加TTS事件监听器
private var isTtsListenerAdded = false
// 是否已注册BLE回调
private var isRegisteredToBle = false
// ASR 事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) {
if (asrEventSink == null) {
FileLogger.w(tag, "无法发送ASR事件:事件通道未准备好")
return
}
mainHandler.post {
try {
asrEventSink?.success(event)
} catch (e: Exception) {
FileLogger.e(tag, "发送ASR事件失败: ${e.message}")
}
else -> mapOf(
"type" to event.type.toString().lowercase(),
"params" to event.params
)
}
// 发送事件
sendTtsEvent(eventMap)
}
})
isTtsListenerAdded = true
}
}
// ASR方法处理器
inner class AsrMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val supportedLanguages = call.argument<List<String>>("supportedLanguages") ?: listOf("zh-CN")
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
try {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
// 选择音频源类型
val audioSourceType = if (useExternalAudio) {
AzureAsrHelper.AudioSourceType.EXTERNAL
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
val success = azureAsrHelper.initialize(
subscriptionKey = subscriptionKey,
region = region,
supportedLanguages = supportedLanguages.toTypedArray(),
audioSourceType = audioSourceType,
)
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
// TTS 事件发送方法
private fun sendTtsEvent(event: Map<String, Any>) {
if (ttsEventSink == null) {
FileLogger.w(tag, "无法发送TTS事件:事件通道未准备好")
return
}
"recognizeOnce" -> {
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
try {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
// 选择音频源类型
val audioSourceType = if (useExternalAudio) {
AzureAsrHelper.AudioSourceType.EXTERNAL
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
// 确保在主线程上发送事件
mainHandler.post {
try {
ttsEventSink?.success(event)
} catch (e: Exception) {
FileLogger.e(tag, "发送TTS事件失败: ${e.message}")
}
// 直接使用同步方式进行一次性识别
azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
// 直接返回结果,不发送事件
result.success(mapOf(
"text" to text,
"detectedLanguage" to detectedLanguage
))
}
override fun onError(error: String) {
result.error("RECOGNITION_ERROR", error, null)
}
},audioSourceType)
} catch (e: Exception) {
result.error("RECOGNITION_ERROR", "识别过程异常: ${e.message}", null)
}
}
"startContinuousRecognition" -> {
val useExternalAudio = call.argument<Boolean>("audioSourceType") ?: false
// 确保事件通道已准备好
if (asrEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null)
return
}
try {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
// 选择音频源类型
val audioSourceType = if (useExternalAudio) {
AzureAsrHelper.AudioSourceType.EXTERNAL
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
// 初始化ASR通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr")
asrChannel.setMethodCallHandler(AsrMethodHandler())
// 初始化TTS通道
ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/tts")
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化ASR事件通道
asrEventChannel =
EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr_events")
asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
asrEventSink = events
}
override fun onCancel(arguments: Any?) {
asrEventSink = null
}
})
// 初始化TTS事件通道
ttsEventChannel =
EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/tts_events")
ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events
setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器
}
override fun onCancel(arguments: Any?) {
ttsEventSink = null
}
})
// 初始化Azure语音服务
azureTtsHelper = AzureTtsHelper(context)
azureAsrHelper = AzureAsrHelper(context)
// 2. 初始化BleService并注册回调
if (BleService.initialize(context)) {
if (BleService.addCallback(this)) {
isRegisteredToBle = true
FileLogger.d(tag, "已注册BLE回调")
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
FileLogger.w(tag, "BLE回调注册失败")
}
val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
sendAsrEvent(mapOf(
"type" to "result",
"text" to text,
"detectedLanguage" to detectedLanguage
))
}
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
sendAsrEvent(mapOf(
"type" to "recognizing",
"text" to recognizing,
"detectedLanguage" to detectedLanguage
))
}
override fun onSessionStarted() {
sendAsrEvent(mapOf("type" to "sessionStarted"))
}
override fun onSessionStopped() {
sendAsrEvent(mapOf("type" to "sessionStopped"))
}
override fun onCanceled(reason: String, errorDetails: String) {
sendAsrEvent(mapOf(
"type" to "canceled",
"reason" to reason,
"errorDetails" to errorDetails
))
}
override fun onError(error: String) {
sendAsrEvent(mapOf(
"type" to "error",
"message" to error
))
}
},audioSourceType)
result.success(success)
} catch (e: Exception) {
result.error("START_RECOGNITION_ERROR", e.message, null)
}
}
"stopContinuousRecognition" -> {
try {
val success = azureAsrHelper.stopContinuousRecognition()
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", e.message, null)
}
}
"isContinuousRecognitionActive" -> {
result.success(azureAsrHelper.isContinuousRecognitionActive())
}
"dispose" -> {
azureAsrHelper.dispose()
result.success(true)
}
"pushAudioData" -> {
val data = call.argument<ByteArray>("data") ?: return result.error("INVALID_ARGUMENTS", "音频数据不能为空", null)
azureAsrHelper.pushAudioData(data)
result.success(true)
} else {
FileLogger.e(tag, "BLE服务初始化失败")
}
else -> {
result.notImplemented()
}
/**
* 设置TTS事件监听器
*/
private fun setupTtsEventListener() {
if (!isTtsListenerAdded) {
// 添加TTS事件监听器
azureTtsHelper.addListener(object : TtsEventListener {
override fun onEvent(event: TtsEvent) {
// 将TTS事件转换为Flutter可理解的格式
val eventMap = when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> mapOf(
"type" to "synthesis_started"
)
TtsEventType.SYNTHESIS_COMPLETED -> mapOf(
"type" to "synthesis_completed"
)
TtsEventType.SYNTHESIS_CANCELED -> {
val params = event.params
mapOf(
"type" to "synthesis_canceled",
"reason" to (params["reason"] ?: ""),
"errorDetails" to (params["errorDetails"] ?: "")
)
}
TtsEventType.ERROR -> {
val params = event.params
mapOf(
"type" to "error",
"errorCode" to (params["errorCode"] ?: "UNKNOWN_ERROR"),
"errorMessage" to (params["errorMessage"] ?: "未知错误")
)
}
else -> mapOf(
"type" to event.type.toString().lowercase(),
"params" to event.params
)
}
// 发送事件
sendTtsEvent(eventMap)
}
})
isTtsListenerAdded = true
}
}
}
}
// TTS方法处理器
inner class TtsMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false
try {
// 初始化TTS引擎(使用新的接口方法)
val success = azureTtsHelper.initialize(
ttsAppId = "", // Azure TTS不需要appId
ttsAppToken = subscriptionKey,
ttsResource = region,
language = "zh-CN"
)
// 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器
if (success && useCustomAudioOutput) {
// 创建SimpleAudioPlayer实例
val customPlayer = SimpleAudioPlayer(context)
// 获取并设置音频输出流
val outputStream = customPlayer.getAudioOutputStream()
if (outputStream != null) {
azureTtsHelper.setCustomAudioOutputStream(outputStream)
}
// ASR方法处理器
inner class AsrMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val supportedLanguages =
call.argument<List<String>>("supportedLanguages") ?: listOf("zh-CN")
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
try {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
// 选择音频源类型
val audioSourceType = if (useExternalAudio) {
AzureAsrHelper.AudioSourceType.EXTERNAL
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
val success = azureAsrHelper.initialize(
subscriptionKey = subscriptionKey,
region = region,
supportedLanguages = supportedLanguages.toTypedArray(),
audioSourceType = audioSourceType,
)
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
}
"recognizeOnce" -> {
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false
try {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
// 选择音频源类型
val audioSourceType = if (useExternalAudio) {
AzureAsrHelper.AudioSourceType.EXTERNAL
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
// 直接使用同步方式进行一次性识别
azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
// 直接返回结果,不发送事件
result.success(
mapOf(
"text" to text,
"detectedLanguage" to detectedLanguage
)
)
}
override fun onError(error: String) {
result.error("RECOGNITION_ERROR", error, null)
}
}, audioSourceType)
} catch (e: Exception) {
result.error("RECOGNITION_ERROR", "识别过程异常: ${e.message}", null)
}
}
"startContinuousRecognition" -> {
val useExternalAudio = call.argument<Boolean>("audioSourceType") ?: false
// 确保事件通道已准备好
if (asrEventSink == null) {
result.error(
"EVENT_CHANNEL_NOT_READY",
"事件通道未准备好,无法开始连续识别",
null
)
return
}
try {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") //
// 选择音频源类型
val audioSourceType = if (useExternalAudio) {
AzureAsrHelper.AudioSourceType.EXTERNAL
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
val success = azureAsrHelper.startContinuousRecognition(object :
AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
sendAsrEvent(
mapOf(
"type" to "result",
"text" to text,
"detectedLanguage" to detectedLanguage
)
)
}
override fun onRecognizing(
recognizing: String,
detectedLanguage: String
) {
sendAsrEvent(
mapOf(
"type" to "recognizing",
"text" to recognizing,
"detectedLanguage" to detectedLanguage
)
)
}
override fun onSessionStarted() {
sendAsrEvent(mapOf("type" to "sessionStarted"))
}
override fun onSessionStopped() {
sendAsrEvent(mapOf("type" to "sessionStopped"))
}
override fun onCanceled(reason: String, errorDetails: String) {
sendAsrEvent(
mapOf(
"type" to "canceled",
"reason" to reason,
"errorDetails" to errorDetails
)
)
}
override fun onError(error: String) {
sendAsrEvent(
mapOf(
"type" to "error",
"message" to error
)
)
}
}, audioSourceType)
result.success(success)
} catch (e: Exception) {
result.error("START_RECOGNITION_ERROR", e.message, null)
}
}
"stopContinuousRecognition" -> {
try {
val success = azureAsrHelper.stopContinuousRecognition()
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", e.message, null)
}
}
"isContinuousRecognitionActive" -> {
result.success(azureAsrHelper.isContinuousRecognitionActive())
}
"dispose" -> {
azureAsrHelper.dispose()
result.success(true)
}
"pushAudioData" -> {
val data = call.argument<ByteArray>("data") ?: return result.error(
"INVALID_ARGUMENTS",
"音频数据不能为空",
null
)
azureAsrHelper.pushAudioData(data)
result.success(true)
}
"enableRecord" -> {
azureAsrHelper.enableRecord()
result.success(true)
}
"stopRecord" -> {
azureAsrHelper.stopRecord()
result.success(true)
}
else -> {
result.notImplemented()
}
}
// 确保设置了TTS事件监听器
setupTtsEventListener()
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", "TTS初始化失败: ${e.message}", null)
}
}
"setVoice" -> {
val voiceName = call.argument<String>("voiceName") ?: return result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
result.success(azureTtsHelper.setVoice(voiceName))
}
"speakText" -> {
val text = call.argument<String>("text") ?: return result.error("INVALID_ARGUMENTS", "文本不能为空", null)
// 直接调用speakOnce方法
try {
val success = azureTtsHelper.speakOnce(text)
result.success(success)
} catch (e: Exception) {
result.error("SPEAK_ERROR", "语音合成失败: ${e.message}", null)
}
}
"stopSpeaking" -> {
// 直接调用stop方法
try {
val success = azureTtsHelper.stop()
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", "停止语音合成失败: ${e.message}", null)
}
}
"isSpeaking" -> {
result.success(azureTtsHelper.isSpeaking())
}
// TTS方法处理器
inner class TtsMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val useCustomAudioOutput =
call.argument<Boolean>("useCustomAudioOutput") ?: false
try {
// 初始化TTS引擎(使用新的接口方法)
val success = azureTtsHelper.initialize(
ttsAppId = "", // Azure TTS不需要appId
ttsAppToken = subscriptionKey,
ttsResource = region,
language = "zh-CN"
)
// 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器
if (success && useCustomAudioOutput) {
// 创建SimpleAudioPlayer实例
val customPlayer = SimpleAudioPlayer(context)
// 获取并设置音频输出流
val outputStream = customPlayer.getAudioOutputStream()
if (outputStream != null) {
azureTtsHelper.setCustomAudioOutputStream(outputStream)
}
}
// 确保设置了TTS事件监听器
setupTtsEventListener()
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", "TTS初始化失败: ${e.message}", null)
}
}
"setVoice" -> {
val voiceName = call.argument<String>("voiceName")
?: return result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
result.success(azureTtsHelper.setVoice(voiceName))
}
"speakText" -> {
val text = call.argument<String>("text") ?: return result.error(
"INVALID_ARGUMENTS",
"文本不能为空",
null
)
// 直接调用speakOnce方法
try {
val success = azureTtsHelper.speakOnce(text)
result.success(success)
} catch (e: Exception) {
result.error("SPEAK_ERROR", "语音合成失败: ${e.message}", null)
}
}
"stopSpeaking" -> {
// 直接调用stop方法
try {
val success = azureTtsHelper.stop()
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", "停止语音合成失败: ${e.message}", null)
}
}
"isSpeaking" -> {
result.success(azureTtsHelper.isSpeaking())
}
"dispose" -> {
azureTtsHelper.release()
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
"dispose" -> {
azureTtsHelper.release()
result.success(true)
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
asrChannel.setMethodCallHandler(null)
ttsChannel.setMethodCallHandler(null)
asrEventChannel.setStreamHandler(null)
ttsEventChannel.setStreamHandler(null)
try {
if (isRegisteredToBle) {
if (BleService.removeCallback(this)) {
isRegisteredToBle = false
FileLogger.d(tag, "已取消BLE回调")
} else {
FileLogger.w(tag, "BLE回调取消失败")
}
}
// 释放资源
azureTtsHelper.release()
azureAsrHelper.dispose()
} catch (e: Exception) {
FileLogger.e(tag, "释放资源失败: ${e.message}")
}
else -> {
result.notImplemented()
}
// BleService.Callback 接口实现
override fun onScanResult(devices: List<Map<String, Any>>) {
// 不处理扫描结果
}
override fun onConnectionStateChanged(state: Int) {
}
override fun onAudioDataReceived(data: ByteArray) {
if (azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) {
azureAsrHelper.pushAudioData(data)
}
}
// 可选:处理音频数据
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别
*/
override fun onWakeupSignalReceived() {
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
asrChannel.setMethodCallHandler(null)
ttsChannel.setMethodCallHandler(null)
asrEventChannel.setStreamHandler(null)
ttsEventChannel.setStreamHandler(null)
try {
// 释放资源
azureTtsHelper.release()
azureAsrHelper.dispose()
} catch (e: Exception) {
FileLogger.e(tag, "释放资源失败: ${e.message}")
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
// 不处理设备信息
}
}
}

5
local_plugins/azure_speech/pubspec.yaml

@ -10,11 +10,14 @@ environment:
dependencies:
flutter:
sdk: flutter
ble_service:
path: ../ble_service
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

22
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -1,38 +1,18 @@
package com.yunqiinnovation.ble_service
import android.Manifest
import android.app.PendingIntent
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.Context
import android.content.Intent
import android.os.*
import androidx.lifecycle.MutableLiveData
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.*
import android.os.ParcelUuid
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.IntentFilter
import android.os.SystemClock
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
import java.util.concurrent.ConcurrentHashMap
import android.content.pm.ServiceInfo
import java.util.concurrent.CopyOnWriteArrayList
// Import Jieli Opus SDK classes
import com.jieli.jl_audio_decode.opus.model.OpusOption
import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback
import com.jieli.jl_audio_decode.callback.OnStateCallback
import com.jieli.jl_audio_decode.opus.OpusManager
import com.jieli.jl_audio_decode.exceptions.OpusException
import com.yunqiinnovation.ble_service.BleCompanionManager
import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure;
import com.jieli.jl_bt_ota.constant.ErrorCode;
import com.jieli.jl_bt_ota.constant.StateCode;
/**
* BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能
@ -1386,4 +1366,4 @@ object BleService {
}
}
}
}
}

BIN
local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock

Binary file not shown.

2
local_plugins/opus/.gradle/buildOutputCleanup/cache.properties

@ -1,2 +0,0 @@
#Wed May 21 01:03:35 CST 2025
gradle.version=8.9

26
local_plugins/ota/android/src/main/kotlin/com/example/ota/DeviceConnection.kt

@ -1,26 +0,0 @@
package com.jieli.otasdk.model
import android.bluetooth.BluetoothDevice
/**
* @author zqjasonZhong
* @since 2022/9/9
* @email zhongzhuocheng@zh-jieli.com
* @desc 设备连接状态
*/
class DeviceConnection(val device: BluetoothDevice?, var state: Int) {
override fun hashCode(): Int {
return device.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other !is DeviceConnection) return false
return device == other.device
}
override fun toString(): String {
return "DeviceConnection(device=$device, state=$state)"
}
}

40
local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt

@ -9,29 +9,18 @@ import io.flutter.plugin.common.MethodChannel.Result
import android.bluetooth.BluetoothDevice
import android.content.Context
import com.jieli.otasdk.tool.ota.OTAManager
import com.jieli.jl_bt_ota.constant.ErrorCode
import com.jieli.jl_bt_ota.constant.StateCode
import com.jieli.jl_bt_ota.interfaces.BtEventCallback
import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback
import com.jieli.jl_bt_ota.model.base.BaseError
import com.jieli.jl_bt_ota.model.response.TargetInfoResponse
import com.jieli.jl_bt_ota.util.BluetoothUtil
import com.jieli.jl_bt_ota.util.CHexConver
import com.jieli.jl_bt_ota.util.JL_Log
import com.jieli.otasdk.tool.bluetooth.BluetoothHelper
import androidx.lifecycle.MutableLiveData
import com.jieli.otasdk.model.DeviceConnection
import android.util.Log
import androidx.annotation.NonNull
import com.jieli.otasdk.tool.config.ConfigHelper
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst
import android.os.Handler
import android.os.Looper
import java.io.File
import java.io.FileOutputStream
import java.io.RandomAccessFile
import io.flutter.plugin.common.EventChannel
class CustomUpdateCallback(
@ -130,7 +119,10 @@ class CustomUpdateCallback(
Log.d(TAG, "升级失败: ${error?.message ?: "未知错误"}")
if (streamSink != null) {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.d(TAG, "升级失败:error?.message ${error?.message}未知错误 error?.code${error?.code}")
Log.d(
TAG,
"升级失败:error?.message ${error?.message}未知错误 error?.code${error?.code}"
)
streamSink.success(
mapOf(
"event" to "onError",
@ -282,8 +274,20 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
bluetoothHelper = BluetoothHelper(context)
// 如果需要额外的初始化逻辑,请在这里添加
otaManager.registerBluetoothCallback(bluetoothCallback)
BleService.addCallback(this)
isRegisteredToBle = true
// 初始化BleService
// 2. 初始化BleService并注册回调
if (BleService.initialize(context)) {
if (BleService.addCallback(this)) {
isRegisteredToBle = true
Log.d(TAG, "已注册BLE回调")
} else {
Log.w(TAG, "BLE回调注册失败")
}
} else {
Log.e(TAG, "BLE服务初始化失败")
}
Log.i("TAG", "onAttachedToEngine")
}
@ -309,6 +313,9 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
otaDevice = bluetoothGatt.device
BleService.disconnect()
} else if (otaDevice != null) {
isDisconnect = true
BleService.disconnect()
} else {
Log.e("TAG", "BluetoothGatt 或 device 为 null,无法执行操作")
sendEventOnMainThread(
@ -321,9 +328,11 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
}
} else if (call.method == "onOtaClose") {
} else if (call.method == "onOtaClose") {
BleService.isEnterOta = false
isDisconnect = false
otaManager.bluetoothHelper.stopScan()
otaManager.bluetoothHelper.disconnectDevice(otaDevice)
} else {
result.notImplemented()
}
@ -345,6 +354,7 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
}
// BleService.Callback 接口实现
override fun onScanResult(devices: List<Map<String, Any>>) {
// 不处理扫描结果
}

3
local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleManager.java

@ -646,9 +646,10 @@ public class BleManager {
} else {
Log.e(TAG, "连接设备失败: ${device.name} (${device.address})");
}
}
mDiscoveredBleDevices.add(device);
handleDiscoveryBle(device, new BleScanInfo().setRawData(scanRecord).setRssi(rssi).setEnableConnect(isBleEnableConnect));
}
}
}

94
local_plugins/ota/lib/ota.dart

@ -2,7 +2,6 @@ import 'ota_platform_interface.dart';
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
class OtaEvent {
static final int NOT_STARTED = -1; // 未开始
@ -57,7 +56,7 @@ class Ota {
// 私有构造函数
Ota._internal() {
// 初始化时设置事件监听
_setupEventListener();
// _setupEventListener();
}
// 公共访问点
@ -66,13 +65,13 @@ class Ota {
static const EventChannel _eventChannel = EventChannel(
'com.example.ota/events',
);
final RxDouble progress = 0.0.obs;
final RxInt type = OtaEvent.NOT_STARTED.obs;
final RxBool isUpgrade = false.obs;
// double progress = 0.0;
// late int type = OtaEvent.NOT_STARTED;
// bool isUpgrade = false;
/// 事件流
Stream<OtaEvent>? _eventStream;
bool _isEventListenerSet = false; // 防止重复监听
// bool _isEventListenerSet = false; // 防止重复监听
/// 获取事件流
Stream<OtaEvent> get eventStream {
@ -90,53 +89,48 @@ class Ota {
onOtaClose();
// 停止事件监听
_eventStream = null;
_isEventListenerSet = false;
// 清理 Rx 对象
progress.close();
type.close();
isUpgrade.value = false;
//_isEventListenerSet = false;
print("Ota 对象资源已释放");
}
/// 内部设置事件监听(只执行一次)
void _setupEventListener() {
if (_isEventListenerSet) return;
eventStream.listen((event) {
if (event.event == 'onProgress' &&
event.type != null &&
event.progress != null) {
print("flutter 收到数据${event.progress}type=${type.value}");
progress.value = event.progress!;
type.value = event.type!;
}
if (event.event == 'onStartOTA') {
type.value = OtaEvent.START_UPGRADING;
isUpgrade.value = true; // 开始升级
}
if (event.event == 'onStopOTA') {
type.value = OtaEvent.UPGRADE_SUCCESS;
isUpgrade.value = false;
}
if (event.event == 'onError' &&
event.code != null &&
event.message != null) {
print("flutter onError${event.code}type=${event.message}");
type.value = OtaEvent.UPGRADE_FAILED;
isUpgrade.value = false;
}
if (event.event == 'NOCONNECTION') {
type.value = OtaEvent.CONNECTION_DISCONNECTION;
isUpgrade.value = false;
}
if (event.event == 'CONNECTION_SUCCESS') {
type.value = OtaEvent.CONNECTION_SUCCESS;
}
});
_isEventListenerSet = true;
}
// /// 内部设置事件监听(只执行一次)
// void _setupEventListener() {
// if (_isEventListenerSet) return;
// eventStream.listen((event) {
// if (event.event == 'onProgress' &&
// event.type != null &&
// event.progress != null) {
// print("flutter 收到数据${event.progress}type=${type}");
// progress = event.progress!;
// type = event.type!;
// }
// if (event.event == 'onStartOTA') {
// type = OtaEvent.START_UPGRADING;
// isUpgrade = true; // 开始升级
// }
// if (event.event == 'onStopOTA') {
// type = OtaEvent.UPGRADE_SUCCESS;
// isUpgrade = false;
// }
// if (event.event == 'onError' &&
// event.code != null &&
// event.message != null) {
// print("flutter onError${event.code}type=${event.message}");
// type = OtaEvent.UPGRADE_FAILED;
// isUpgrade = false;
// }
// if (event.event == 'NOCONNECTION') {
// type = OtaEvent.CONNECTION_DISCONNECTION;
// isUpgrade = false;
// }
// if (event.event == 'CONNECTION_SUCCESS') {
// type = OtaEvent.CONNECTION_SUCCESS;
// }
// });
// _isEventListenerSet = true;
// }
Future<String?> startUpgrading(String? filePath) {
return OtaPlatform.instance.startUpgrading(filePath);

1
local_plugins/ota/pubspec.yaml

@ -11,7 +11,6 @@ dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
get: ^4.6.5
dev_dependencies:
flutter_test:
sdk: flutter

2
pubspec.yaml

@ -21,7 +21,7 @@ dependencies:
audio_session: ^0.1.18
flutter_markdown: ^0.7.7+1
markdown: ^7.1.1
permission_handler: ^11.4.0
permission_handler: ^12.0.0+1
web_socket_channel: ^3.0.2
flutter_dotenv: ^5.1.0
synchronized: ^3.1.0

Loading…
Cancel
Save