Browse Source

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

weicu
wolfplus 1 year ago
parent
commit
45dfe4e0b2
  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. 24
      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. 88
      lib/modules/pairing/controllers/pairing_controller.dart
  12. 171
      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. 184
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  18. 821
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  19. 3
      local_plugins/azure_speech/pubspec.yaml
  20. 20
      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(); bool isContinuousRecognitionActive();
/// 开启录音
Future<bool> enableRecord();
/// 停止录音
Future<bool> stopRecord();
/// 释放资源 /// 释放资源
Future<void> dispose(); Future<void> dispose();
} }

13
lib/data/services/ble_manager.dart

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

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

@ -342,4 +342,28 @@ class AzureAsrService extends GetxService implements AsrService {
return false; 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:deep_voice/core/utils/logger.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import '../../../core/utils/permission_util.dart';
class Message { class Message {
final bool isUser; //标识信息是否来自用户还是AI final bool isUser; //标识信息是否来自用户还是AI
@ -697,7 +698,16 @@ class AgentController extends GetxController {
// 如果要切换到语音模式,需要先检查麦克风权限 // 如果要切换到语音模式,需要先检查麦克风权限
if (isTextInputMode.value) { 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) { if (!hasPermission) {
return; // 权限未获取,保持文本模式 return; // 权限未获取,保持文本模式
} }
@ -715,54 +725,54 @@ class AgentController extends GetxController {
} }
} }
// 检查麦克风权限 // 检查麦克风权限,返回true表示已授权,false表示未授权
Future<bool> _checkMicrophonePermission() async { Future<bool> _checkMicrophonePermission() async {
try { try {
// iOS平台直接返回true,不处理权限问题 // 1.先获取当前麦克风权限状态
if (Platform.isIOS) { final micStatus = await Permission.microphone.status;
// 2.如果麦克风权限已授予,返回true
if (micStatus.isGranted) {
return true; return true;
} }
// 创建待请求权限列表 // 3.创建待请求权限列表(这里只请求麦克风权限)
List<Permission> permissionsToRequest = []; List<Permission> permissionsToRequest = [];
// Android 需要考虑SDK版本不同的权限要求
permissionsToRequest.add(Permission.microphone); permissionsToRequest.add(Permission.microphone);
// 批量请求所有需要的权限 // 4.弹出权限说明对话框(首次拒绝/还没授权时展示)
if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true);
} else {
_showPermissionDialog(false);
}
// 5.请求权限,并获取返回的权限状态集合
final statuses = await permissionsToRequest.request(); final statuses = await permissionsToRequest.request();
// 检查是否所有权限都已授予 // 6.用于记录被拒绝的权限(便于后续判断)
bool allGranted = true;
Permission? deniedPermission; Permission? deniedPermission;
// 7.遍历权限状态集合,检查是否有任何权限被拒绝
statuses.forEach((permission, status) { statuses.forEach((permission, status) {
if (!status.isGranted) { if (!status.isGranted) {
allGranted = false;
deniedPermission = permission; deniedPermission = permission;
Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status'); Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status');
} }
}); });
// 单独检查麦克风权限状态,因为这是主要需求 // // 8. 如果是“永久拒绝”(用户勾选了“不再询问”)
final micStatus = await Permission.microphone.status; // if (micStatus.isPermanentlyDenied) {
// _showPermissionDialog(true);
// return false;
// }
// 如果所有权限都已授予,或至少麦克风权限已授予,返回true // // 9. 普通的拒绝情况,再次弹出说明对话框
if (allGranted || micStatus.isGranted) { // if (deniedPermission != null) {
return true; // _showPermissionDialog(false);
} // }
// 处理特定权限被永久拒绝的情况
if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true);
return false;
}
// 普通拒绝情况
if (deniedPermission != null) {
_showPermissionDialog(false);
}
// 10. 如果权限未被授予,返回 false
return false; return false;
} catch (e) { } catch (e) {
Logger.e(TAG, '检查麦克风权限失败: $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版本 // 获取Android SDK版本
Future<int> _getAndroidSdkVersion() async { Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
@ -784,41 +875,20 @@ class AgentController extends GetxController {
return 0; 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 { Future<void> startVoiceInput() async {
if (isListening.value) return; 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) { if (!hasPermission) {
isTextInputMode.value = true; // 如果没有权限,切回文本模式 isTextInputMode.value = true; // 如果没有权限,切回文本模式
return; return;

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

@ -46,8 +46,6 @@ class MessageBubble extends GetView<AgentController> {
isUser ? MainAxisAlignment.end : MainAxisAlignment.start, isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
//if (!isUser) _buildAvatar(isUser, context),
//const SizedBox(width: 8),
Flexible( Flexible(
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment:
@ -90,21 +88,41 @@ class MessageBubble extends GetView<AgentController> {
const SizedBox(height: 4), const SizedBox(height: 4),
// 识别中的消息不显示时间 // 识别中的消息不显示时间
if (!isRecognizing) if (!isRecognizing)
Text( Row(
timeFormat.format(timestamp), mainAxisAlignment: isUser
style: TextStyle( ? MainAxisAlignment.end
fontSize: 10, : MainAxisAlignment.spaceBetween,
color: Theme.of(context).brightness == Brightness.dark children: [
? Colors.grey[400] // 时间戳(左对齐或右对齐)
: Colors.grey[500], Text(
fontWeight: FontWeight.w400, 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) { Widget _buildMessageContent(BuildContext context) {
// 如果是识别中的消息,添加特殊样式 // 如果是识别中的消息,添加特殊样式

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

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

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

@ -226,7 +226,6 @@ class HomeView extends GetView<HomeController> {
// 设备信息 // 设备信息
SizedBox(height: 12.h), SizedBox(height: 12.h),
Obx(() { Obx(() {
// 如果连接失败且有错误信息,显示错误提示 // 如果连接失败且有错误信息,显示错误提示
if (controller.connectionStatus == ConnectionStatus.failed && if (controller.connectionStatus == ConnectionStatus.failed &&
controller.connectionErrorMsg.isNotEmpty) { controller.connectionErrorMsg.isNotEmpty) {
@ -861,23 +860,26 @@ class HomeView extends GetView<HomeController> {
SizedBox(height: 5.h), // 减少间距 SizedBox(height: 5.h), // 减少间距
// 右下卡片 - AI女友 // 右下卡片 -
// 右下卡片 - 会议助手
_buildFeatureCard( _buildFeatureCard(
title: 'aiCompanion'.tr, // 亲子陪伴 title: 'conferenceAssistant'.tr, // 商务会议助手
description: 'aiCompanionInfo'.tr, // 懂你心意,温暖陪伴 description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译
icon: Icons.favorite, icon: Icons.people,
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [ colors: [
Color(0xFFE91E63).withOpacity(0.3), Color(0xFFab47bc).withOpacity(0.3), // 薰衣草紫
Color(0xFFFF5252).withOpacity(0.3), Color(0xFFec407a).withOpacity(0.3), // 品红
], ],
), ),
onTap: () {}, onTap: () {
Get.toNamed(Routes.meeting);
},
height: 85.h, // 增加高度 height: 85.h, // 增加高度
isDarkMode: isDarkMode, 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; final isUpdateAvailable = true.obs;
// 当前升级进度,范围为 0.0 到 1.0 // 当前升级进度,范围为 0.0 到 1.0
final RxDouble progress = 0.0.obs; // final RxDouble progress = 0.0.obs;
// OTA 单例实例,用于处理升级相关逻辑 // OTA 单例实例,用于处理升级相关逻辑
final jlota = Ota.instance; final jlota = Ota.instance;
@ -52,7 +52,12 @@ class OtaUpgradeController extends GetxController {
final _bleManager = Get.find<BleManager>(); final _bleManager = Get.find<BleManager>();
final maxRetryAttempts = 3; // 最大重试次数 final maxRetryAttempts = 3; // 最大重试次数
int currentRetryCount = 0; int currentRetryCount = 0;
final RxBool isFetchingDeviceInfo = true.obs; final RxBool isFetchingDeviceInfo = true.obs;
final RxDouble progress = 0.0.obs; // 控制器自己的可观察属性
final RxInt type = OtaEvent.NOT_STARTED.obs;
final RxBool isUpgrade = false.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -62,6 +67,7 @@ class OtaUpgradeController extends GetxController {
@override @override
void onClose() { void onClose() {
jlota.onOtaClose(); jlota.onOtaClose();
isUpgrade.value = false;
super.onClose(); super.onClose();
} }
@ -109,6 +115,7 @@ class OtaUpgradeController extends GetxController {
} }
Future<void> _getCurrentVersion() async { Future<void> _getCurrentVersion() async {
_listenToEvents();
try { try {
isFetchingDeviceInfo.value = true; isFetchingDeviceInfo.value = true;
errorMessage.value = ''; errorMessage.value = '';
@ -142,8 +149,20 @@ class OtaUpgradeController extends GetxController {
deviceInfo.firmwareProductId.isEmpty) { deviceInfo.firmwareProductId.isEmpty) {
throw "获取设备信息失败,请检查连接"; 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}'); print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}');
// API请求 // API请求
final response = await Api.getequipmentinfo({ final response = await Api.getequipmentinfo({
@ -256,7 +275,7 @@ class OtaUpgradeController extends GetxController {
headers: {'Accept-Encoding': 'gzip'}, headers: {'Accept-Encoding': 'gzip'},
)); ));
jlota.type.value = DOWNLOADING_UP_FILES; type.value = DOWNLOADING_UP_FILES;
try { try {
final Directory? downloadsDir = await getDownloadsDirectory(); final Directory? downloadsDir = await getDownloadsDirectory();
if (downloadsDir == null) { if (downloadsDir == null) {
@ -352,4 +371,45 @@ class OtaUpgradeController extends GetxController {
return; return;
// isUpgrade.value = false; // 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) { Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return WillPopScope(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[100], onWillPop: () async {
appBar: AppBar( if (controller.isUpgrade.value) {
automaticallyImplyLeading: false, // 显示确认弹窗
centerTitle: true, await Get.dialog(
elevation: 0, AlertDialog(
backgroundColor: isDarkMode ? Colors.black : Colors.white, title: Text('提示'),
leading: IconButton( content: Text('当前正在升级,退出将中断升级进程,确定要退出吗?'),
icon: Icon( actions: [
Icons.arrow_back_ios, TextButton(
size: 20.sp, onPressed: () => Get.back(), // 关闭弹窗
color: isDarkMode ? Colors.white : Colors.black, 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,
title: Text( style: TextStyle(
'固件升级'.tr, fontSize: 20.sp,
style: TextStyle( color: isDarkMode ? Colors.white : Colors.black,
fontSize: 20.sp, ),
color: isDarkMode ? Colors.white : Colors.black,
), ),
), ),
), body: SingleChildScrollView(
body: SingleChildScrollView( child: Padding(
child: Padding( padding: EdgeInsets.all(20),
padding: EdgeInsets.all(20), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ // 设备信息卡片
// 设备信息卡片 Card(
Card( elevation: 2,
elevation: 2, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16), ),
), color: isDarkMode ? Colors.grey[900] : Colors.white,
color: isDarkMode ? Colors.grey[900] : Colors.white, child: Padding(
child: Padding( padding: EdgeInsets.all(20),
padding: EdgeInsets.all(20), child: Column(
child: Column( children: [
children: [ _buildDeviceIcon(isDarkMode),
_buildDeviceIcon(isDarkMode), SizedBox(height: 20),
SizedBox(height: 20), _buildVersionInfo(isDarkMode),
_buildVersionInfo(isDarkMode), ],
], ),
), ),
), ),
), SizedBox(height: 20),
SizedBox(height: 20),
// 更新说明 // 更新说明
_buildUpdateNotes(isDarkMode), _buildUpdateNotes(isDarkMode),
SizedBox(height: 20), SizedBox(height: 20),
// 状态显示区域 // 状态显示区域
Obx(() { Obx(() {
if (controller.isFetchingDeviceInfo.value) { if (controller.isFetchingDeviceInfo.value) {
return _buildLoadingIndicator(isDarkMode); return _buildLoadingIndicator(isDarkMode);
} }
if (controller.errorMessage.value.isNotEmpty) { if (controller.errorMessage.value.isNotEmpty) {
return _buildErrorInfo(isDarkMode); return _buildErrorInfo(isDarkMode);
} }
return _buildUpdateButton(isDarkMode); return _buildUpdateButton(isDarkMode);
}), }),
], ],
),
), ),
), ),
), ),
@ -227,7 +282,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
); );
Widget _buildUpdateButton(bool isDarkMode) { Widget _buildUpdateButton(bool isDarkMode) {
return controller.jlota.isUpgrade.value return controller.isUpgrade.value
? _buildProgressIndicator(isDarkMode) ? _buildProgressIndicator(isDarkMode)
: Container( : Container(
width: double.infinity, width: double.infinity,
@ -276,8 +331,8 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
Widget _buildProgressIndicator(bool isDarkMode) { Widget _buildProgressIndicator(bool isDarkMode) {
return Obx(() { return Obx(() {
final progress = controller.jlota.progress.value; final progress = controller.progress.value;
final type = controller.jlota.type.value; final type = controller.type.value;
// // 处理升级失败状态:弹出 snackbar 并显示错误提示 // // 处理升级失败状态:弹出 snackbar 并显示错误提示
// if (type == OtaUpgradeController.UPGRADE_FAILED || // if (type == OtaUpgradeController.UPGRADE_FAILED ||

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

@ -7,6 +7,7 @@ import '../../../core/utils/logger.dart';
import 'dart:io'; import 'dart:io';
import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
import '../../../core/utils/permission_util.dart';
/// 配对状态枚举 /// 配对状态枚举
enum PairingState { enum PairingState {
@ -178,13 +179,24 @@ class PairingController extends GetxController {
Logger.i(_tag, '开始执行初始化流程'); Logger.i(_tag, '开始执行初始化流程');
isLoading.value = true; isLoading.value = true;
statusDescription.value = '正在检查权限...'; statusDescription.value = '正在初始化...';
try { try {
// 初始视觉反馈延迟
await Future.delayed(const Duration(milliseconds: 800));
// 更新状态让用户知道正在进行权限检查
statusDescription.value = '正在检查所需权限...';
// iOS平台跳过权限检查 // iOS平台跳过权限检查
if (!Platform.isIOS) { if (!Platform.isIOS) {
// 添加额外视觉反馈延迟
await Future.delayed(const Duration(milliseconds: 600));
final hasPermission = await _checkAndRequestPermissions(); final hasPermission = await _checkAndRequestPermissions();
if (!hasPermission) { if (!hasPermission) {
statusDescription.value = '缺少必要的权限';
await Future.delayed(const Duration(milliseconds: 500));
_transition(PairingState.exitNoPermission); _transition(PairingState.exitNoPermission);
return; return;
} }
@ -193,6 +205,10 @@ class PairingController extends GetxController {
// 设置蓝牙状态监听 // 设置蓝牙状态监听
_setupBluetoothListener(); _setupBluetoothListener();
// 更新状态让用户知道正在进行蓝牙检查
statusDescription.value = '检查蓝牙状态...';
await Future.delayed(const Duration(milliseconds: 400));
// 检查蓝牙状态 // 检查蓝牙状态
final btState = BluetoothManager.to.bluetoothState; final btState = BluetoothManager.to.bluetoothState;
Logger.i(_tag, '当前蓝牙状态: $btState'); Logger.i(_tag, '当前蓝牙状态: $btState');
@ -200,10 +216,14 @@ class PairingController extends GetxController {
if (btState == 'on' || btState == 'unknown') { if (btState == 'on' || btState == 'unknown') {
_transition(PairingState.btReady); _transition(PairingState.btReady);
} else { } else {
statusDescription.value = '蓝牙未开启';
await Future.delayed(const Duration(milliseconds: 500));
_transition(PairingState.exitBtOff); _transition(PairingState.exitBtOff);
} }
} catch (e) { } catch (e) {
Logger.e(_tag, 'Init failed: $e'); Logger.e(_tag, 'Init failed: $e');
statusDescription.value = '初始化出错';
await Future.delayed(const Duration(milliseconds: 500));
_transition(PairingState.exitNoPermission); _transition(PairingState.exitNoPermission);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -508,23 +528,75 @@ class PairingController extends GetxController {
return true; return true;
} }
// Android权限检查 // Android权限检查 - 使用 PermissionUtil
final permissions = <Permission>[]; final context = Get.context;
permissions.add(Permission.locationWhenInUse); if (context == null) {
Logger.e(_tag, '无法获取有效上下文,跳过权限请求');
return false;
}
// 根据 Android 版本确定需要的权限
List<Permission> requiredPermissions = [
Permission.locationWhenInUse,
];
if (Platform.isAndroid && await _getAndroidSdkVersion() >= 31) { if (Platform.isAndroid && await _getAndroidSdkVersion() >= 31) {
permissions.add(Permission.bluetoothScan); requiredPermissions.add(Permission.bluetoothScan);
permissions.add(Permission.bluetoothConnect); 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 true;
return statuses.values.every((status) => status.isGranted);
} catch (e) { } catch (e) {
Logger.e(_tag, '权限检查失败: $e'); Logger.e(_tag, '权限检查失败: $e');
return false; 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() { void _setupBluetoothListener() {
_bluetoothStateSubscription?.cancel(); _bluetoothStateSubscription?.cancel();

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

@ -4,6 +4,7 @@ import 'package:permission_handler/permission_handler.dart';
import '../controllers/settings_controller.dart'; import '../controllers/settings_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui'; import 'dart:ui';
import 'package:deep_voice/core/utils/permission_util.dart'; // 引入PermissionUtil
class PermissionsView extends GetView<SettingsController> { class PermissionsView extends GetView<SettingsController> {
const PermissionsView({Key? key}) : super(key: key); const PermissionsView({Key? key}) : super(key: key);
@ -52,24 +53,28 @@ class PermissionsView extends GetView<SettingsController> {
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: [ children: [
// 通讯权限组 // 通讯权限组
_buildPermissionSection('通讯权限', Icons.phone_android, [ _buildPermissionSection(
Permission.phone, '通讯权限',
Permission.sms Icons.phone_android,
], permissionStatuses, isDarkMode), [Permission.phone, Permission.sms],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h), SizedBox(height: 20.h),
// 日历权限组 // 日历权限组
_buildPermissionSection('日历权限', Icons.calendar_today, [ _buildPermissionSection(
Permission.calendarFullAccess '日历权限',
], permissionStatuses, isDarkMode), Icons.calendar_today,
[Permission.calendarFullAccess],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h), SizedBox(height: 20.h),
// 位置权限组 // 位置权限组
_buildPermissionSection('位置权限', Icons.location_on, [ _buildPermissionSection('位置权限', Icons.location_on,
Permission.location [Permission.location], permissionStatuses, isDarkMode),
], permissionStatuses, isDarkMode),
], ],
); );
}, },
@ -78,8 +83,12 @@ class PermissionsView extends GetView<SettingsController> {
} }
// 构建权限组区域 // 构建权限组区域
Widget _buildPermissionSection(String title, IconData icon, List<Permission> permissions, Widget _buildPermissionSection(
Map<Permission, PermissionStatus> statuses, bool isDarkMode) { String title,
IconData icon,
List<Permission> permissions,
Map<Permission, PermissionStatus> statuses,
bool isDarkMode) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -119,7 +128,8 @@ class PermissionsView extends GetView<SettingsController> {
? ImageFilter.blur(sigmaX: 5, sigmaY: 5) ? ImageFilter.blur(sigmaX: 5, sigmaY: 5)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0), : ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Column( child: Column(
children: _buildPermissionItems(permissions, statuses, isDarkMode), children:
_buildPermissionItems(permissions, statuses, isDarkMode),
), ),
), ),
), ),
@ -133,39 +143,39 @@ class PermissionsView extends GetView<SettingsController> {
Map<Permission, PermissionStatus> statuses, bool isDarkMode) { Map<Permission, PermissionStatus> statuses, bool isDarkMode) {
final List<Widget> items = []; final List<Widget> items = [];
// 获取描述文本 // 获取名称
String getDescription(Permission permission) { String getName(Permission permission) {
switch (permission) { switch (permission) {
case Permission.phone: case Permission.phone:
return '用于拨打电话功能'; return '电话';
case Permission.sms: case Permission.sms:
return '用于发送短信功能'; return '短信';
case Permission.calendarWriteOnly: case Permission.calendarWriteOnly:
return '用于创建日历事件和提醒'; return '日历写入';
case Permission.calendarFullAccess: case Permission.calendarFullAccess:
return '用于读取和管理日历事件及提醒'; return '日历完整访问';
case Permission.location: case Permission.location:
return '用于获取当前位置信息'; return '位置信息';
default: default:
return '应用功能需要'; return permission.toString();
} }
} }
// 获取名称 // 获取描述文本
String getName(Permission permission) { String getDescription(Permission permission) {
switch (permission) { switch (permission) {
case Permission.phone: case Permission.phone:
return '电话'; return '用于拨打电话功能';
case Permission.sms: case Permission.sms:
return '短信'; return '用于发送短信功能';
case Permission.calendarWriteOnly: case Permission.calendarWriteOnly:
return '日历写入'; return '用于创建日历事件和提醒';
case Permission.calendarFullAccess: case Permission.calendarFullAccess:
return '日历完整访问'; return '用于读取和管理日历事件及提醒';
case Permission.location: case Permission.location:
return '位置信息'; return '用于获取当前位置信息';
default: default:
return permission.toString(); return '应用功能需要';
} }
} }
@ -262,7 +272,7 @@ class PermissionsView extends GetView<SettingsController> {
color = Colors.red; color = Colors.red;
} else if (status.isPermanentlyDenied) { } else if (status.isPermanentlyDenied) {
text = '需要在系统设置中开启'.tr; text = '需要在系统设置中开启'.tr;
color = Colors.grey; color = Colors.orange;
} else if (status.isLimited) { } else if (status.isLimited) {
text = '部分授权'.tr; text = '部分授权'.tr;
color = Colors.orange; color = Colors.orange;
@ -282,43 +292,82 @@ class PermissionsView extends GetView<SettingsController> {
} }
// 处理权限请求 // 处理权限请求
Future<void> _handlePermissionAction(Permission permission, PermissionStatus status) async { Future<void> _handlePermissionAction(
if (status.isPermanentlyDenied) { Permission permission, PermissionStatus status) async {
// 打开应用设置 try {
controller.openSettings(); final context = Get.context;
return; if (context == null) return;
}
if (!status.isGranted) { final permissionName = _getPermissionName(permission);
// 请求权限 final explanationText = _getExplanationText(permission);
final result = await controller.requestPermission(permission);
if (result.isGranted) { // 使用 PermissionUtil 请求权限
Get.snackbar('成功'.tr, '权限已授予'.tr, snackPosition: SnackPosition.BOTTOM); final hasPermission = await PermissionUtil.instance.requestPermission(
} else if (result.isPermanentlyDenied) { context: context,
Get.dialog( permissionType: permission,
AlertDialog( permissionName: permissionName,
title: Text('需要权限'.tr), explanationText: explanationText,
content: Text('此权限需要在设置中手动开启'.tr), permanentDenialText: '您已永久拒绝$permissionName权限,请前往应用设置开启',
actions: [ settingsButtonText: '去设置',
TextButton( cancelButtonText: '取消',
onPressed: () => Get.back(), );
child: Text('取消'.tr),
), if (hasPermission) {
TextButton( Get.snackbar(
onPressed: () { '成功'.tr,
Get.back(); '$permissionName权限已授予'.tr,
controller.openSettings(); snackPosition: SnackPosition.BOTTOM,
}, backgroundColor: Colors.green,
child: Text('去设置'.tr), colorText: Colors.white,
),
],
),
); );
} }
// 刷新界面 // 刷新界面,重新获取权限状态
Get.forceAppUpdate(); 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 isRecognizing = false.obs;
final isTranslating = false.obs; final isTranslating = false.obs;
final isTtsEnabled = true.obs; final isTtsEnabled = true.obs;
final isRecording = false.obs;
// 语言相关 // 语言相关
final sourceLanguage = '中文(简体)'.obs; final sourceLanguage = '中文(简体)'.obs;
final targetLanguage = '英语'.obs; final targetLanguage = '英语'.obs;
@ -193,6 +193,16 @@ class TranslationController extends GetxController {
// 切换TTS功能 // 切换TTS功能
void toggleTts() => isTtsEnabled.toggle(); void toggleTts() => isTtsEnabled.toggle();
// 切换录音功能
Future<void> toggleisRecord() async {
isRecording.toggle();
if (isRecording.value) {
await _asrService.enableRecord();
} else {
await _asrService.stopRecord();
}
}
// 开始语音识别 // 开始语音识别
Future<void> startRecognition() async { Future<void> startRecognition() async {
Logger.info('开始语音识别'); Logger.info('开始语音识别');
@ -233,15 +243,18 @@ class TranslationController extends GetxController {
_bleManager.openDecoder(); _bleManager.openDecoder();
// 发送ble音乐或者通话远端声音 // 发送ble音乐或者通话远端声音
_audioSourceType = true; _audioSourceType = true;
isTtsEnabled.value = false; // 禁用TTS以避免干扰
Logger.info('发送ble音乐或者通话远端声音'); Logger.info('发送ble音乐或者通话远端声音');
} else if (currentMode.value == "call") { } else if (currentMode.value == "call") {
// 发送ble系统mic和dac(音乐或者通话远端)声音 // 发送ble系统mic和dac(音乐或者通话远端)声音
_bleManager.openA2DPDecoder(); _bleManager.openA2DPDecoder();
_audioSourceType = true; _audioSourceType = true;
isTtsEnabled.value = true;
Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音');
} else { } else {
// 开始连续语音识别 // 开始连续语音识别
_audioSourceType = false; _audioSourceType = false;
isTtsEnabled.value = true;
} }
final recognitionStream = final recognitionStream =
await _asrService.startContinuousRecognition(_audioSourceType); await _asrService.startContinuousRecognition(_audioSourceType);
@ -476,6 +489,11 @@ class TranslationController extends GetxController {
// 停止所有操作 // 停止所有操作
Future<void> stopAll() async { Future<void> stopAll() async {
try { try {
if (currentMode.value == "audioVideo" || currentMode.value == "call") {
_bleManager.closeCodec();
// 发送ble音乐或者通话远端声音
Logger.info('发送ble音乐或者通话远端声音');
}
if (isRecognizing.value) { if (isRecognizing.value) {
await stopRecognition(); await stopRecognition();
} }

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

@ -87,13 +87,13 @@ class TranslationView extends GetView<TranslationController> {
// TTS toggle button // TTS toggle button
Obx(() => IconButton( Obx(() => IconButton(
icon: Icon( icon: Icon(
controller.isTtsEnabled.value ? Icons.mic : Icons.mic_off, controller.isRecording.value ? Icons.mic : Icons.mic_off,
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
size: 24.sp, size: 24.sp,
), ),
// onPressed: controller.toggleTts,默认播放 // onPressed: controller.toggleTts,默认播放
onPressed: controller.toggleTts, onPressed: controller.toggleisRecord,
tooltip: '自动播放翻译结果', 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 package com.yunqiinnovation.agent_service
import android.content.Context import android.content.Context
import com.yunqiinnovation.agent_service.AgentService
import com.yunqiinnovation.agent_service.AgentServiceListener
import android.util.Log import android.util.Log
import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst import com.yunqiinnovation.ble_service.BleConst

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

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

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

@ -28,6 +28,7 @@ import java.util.concurrent.TimeUnit
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.RandomAccessFile import java.io.RandomAccessFile
/** /**
* Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别 * Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别
* *
@ -53,12 +54,16 @@ class AzureAsrHelper(private val context: Context) {
private var region = "" private var region = ""
// 音频源配置 // 音频源配置
private var audioSourceType = AudioSourceType.MICROPHONE var audioSourceType = AudioSourceType.MICROPHONE
// 音频处理 // 音频处理
private var microphoneStream: MicrophoneStream? = null private var microphoneStream: MicrophoneStream? = null
private var externalAudioStream: ExternalAudioPullStream? = null private var externalAudioStream: ExternalAudioPullStream? = null
// 录音文件处理
private var recordfile: RecordingFile? = null
private var isRecording = false
/** /**
* 音频来源类型 * 音频来源类型
*/ */
@ -135,7 +140,8 @@ class AzureAsrHelper(private val context: Context) {
// 设置分段策略为时间模式 // 设置分段策略为时间模式
setProperty("Speech_SegmentationStrategy", "Time") setProperty("Speech_SegmentationStrategy", "Time")
} }
// 录音文件类
recordfile = RecordingFile()
// 创建识别器 // 创建识别器
val setupSuccess = setupRecognizer() val setupSuccess = setupRecognizer()
if (setupSuccess) { if (setupSuccess) {
@ -162,7 +168,8 @@ class AzureAsrHelper(private val context: Context) {
when (audioSourceType) { when (audioSourceType) {
AudioSourceType.MICROPHONE -> { 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 isSpeakerphoneOn = audioManager.isSpeakerphoneOn
// 检查是否连接了有线耳机 // 检查是否连接了有线耳机
@ -188,6 +195,7 @@ class AzureAsrHelper(private val context: Context) {
audioConfig = AudioConfig.fromDefaultMicrophoneInput() audioConfig = AudioConfig.fromDefaultMicrophoneInput()
} }
} }
AudioSourceType.EXTERNAL -> { AudioSourceType.EXTERNAL -> {
// 改用拉流方式处理外部音频 // 改用拉流方式处理外部音频
setupExternalAudioStream() setupExternalAudioStream()
@ -196,7 +204,8 @@ class AzureAsrHelper(private val context: Context) {
// 创建识别器 // 创建识别器
recognizer = if (isAutoDetectLanguage) { recognizer = if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) val autoDetectConfig =
AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig) SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig)
} else { } else {
SpeechRecognizer(speechConfig, audioConfig) SpeechRecognizer(speechConfig, audioConfig)
@ -285,6 +294,7 @@ class AzureAsrHelper(private val context: Context) {
// 使用拉流模式,将数据推入队列 // 使用拉流模式,将数据推入队列
externalAudioStream?.pushAudio(data) externalAudioStream?.pushAudio(data)
} }
/** /**
@ -292,12 +302,15 @@ class AzureAsrHelper(private val context: Context) {
* *
* @param callback 识别结果回调 * @param callback 识别结果回调
*/ */
fun recognizeOnce(callback: RecognizeCallback, audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE) { fun recognizeOnce(
callback: RecognizeCallback,
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE
) {
if (speechConfig == null) { if (speechConfig == null) {
callback.onError("语音服务未初始化") callback.onError("语音服务未初始化")
return return
} }
this.audioSourceType = audioSourceType this.audioSourceType = audioSourceType
// 确保不在连续识别中 // 确保不在连续识别中
if (isContinuousRecognitionActive) { if (isContinuousRecognitionActive) {
stopContinuousRecognition() stopContinuousRecognition()
@ -320,7 +333,8 @@ class AzureAsrHelper(private val context: Context) {
stopAudioProcessing() stopAudioProcessing()
if (result != null && result.reason == ResultReason.RecognizedSpeech) { 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) callback.onResult(result.text, detectedLanguage)
} else { } else {
callback.onError("未能识别语音") callback.onError("未能识别语音")
@ -337,7 +351,10 @@ class AzureAsrHelper(private val context: Context) {
* @param callback 连续识别结果回调 * @param callback 连续识别结果回调
* @return 是否成功开始识别 * @return 是否成功开始识别
*/ */
fun startContinuousRecognition(callback: ContinuousRecognizeCallback, audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE): Boolean { fun startContinuousRecognition(
callback: ContinuousRecognizeCallback,
audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE
): Boolean {
if (speechConfig == null) { if (speechConfig == null) {
callback.onError("语音服务未初始化") callback.onError("语音服务未初始化")
return false return false
@ -363,7 +380,10 @@ class AzureAsrHelper(private val context: Context) {
// 开始连续识别 // 开始连续识别
recognizer?.startContinuousRecognitionAsync() recognizer?.startContinuousRecognitionAsync()
isContinuousRecognitionActive = true isContinuousRecognitionActive = true
Log.e(tag, "开始连续语音识别 是否录音${isRecording}")
if (isRecording) {
enableRecord()
}
return true return true
} catch (e: Exception) { } catch (e: Exception) {
stopAudioProcessing() stopAudioProcessing()
@ -465,7 +485,8 @@ class AzureAsrHelper(private val context: Context) {
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
// 停止录音
recordfile!!.closeFile()
// 会话结束事件会设置isContinuousRecognitionActive = false // 会话结束事件会设置isContinuousRecognitionActive = false
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -475,7 +496,7 @@ class AzureAsrHelper(private val context: Context) {
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
recordfile!!.closeFile()
// 尝试强制关闭识别器 // 尝试强制关闭识别器
try { try {
recognizer?.close() recognizer?.close()
@ -507,7 +528,8 @@ class AzureAsrHelper(private val context: Context) {
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
// 停止录音
recordfile!!.closeFile()
// 释放recognizer // 释放recognizer
recognizer?.close() recognizer?.close()
recognizer = null recognizer = null
@ -549,6 +571,7 @@ class AzureAsrHelper(private val context: Context) {
// 拉流模式不需要额外启动,SDK会自动拉取数据 // 拉流模式不需要额外启动,SDK会自动拉取数据
// 无需执行任何操作 // 无需执行任何操作
} }
AudioSourceType.EXTERNAL -> { AudioSourceType.EXTERNAL -> {
// 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData // 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData
} }
@ -580,6 +603,30 @@ 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提供音频数据 * 实现PullAudioInputStreamCallback,为Azure SDK提供音频数据
@ -668,7 +715,8 @@ class AzureAsrHelper(private val context: Context) {
/** /**
* 获取音频格式 * 获取音频格式
*/ */
fun getFormat(): AudioStreamFormat = AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort()) fun getFormat(): AudioStreamFormat =
AudioStreamFormat.getWaveFormatPCM(16000.toLong(), 16.toShort(), 1.toShort())
/** /**
* Azure SDK 调用此方法获取音频数据 * Azure SDK 调用此方法获取音频数据
@ -734,6 +782,8 @@ class AzureAsrHelper(private val context: Context) {
* @param data 音频数据 * @param data 音频数据
*/ */
fun pushAudio(data: ByteArray) { fun pushAudio(data: ByteArray) {
// 将音频数据保存成wav格式的音频文件
recordfile?.saveAudioDataToWav(data)
if (!closed) { if (!closed) {
queue.offer(data) queue.offer(data)
} }
@ -756,6 +806,7 @@ class AzureAsrHelper(private val context: Context) {
val toCopy = minOf(chunk.size, buffer.size) val toCopy = minOf(chunk.size, buffer.size)
System.arraycopy(chunk, 0, buffer, 0, toCopy) System.arraycopy(chunk, 0, buffer, 0, toCopy)
Log.i(tag, "外部音频:")
return toCopy return toCopy
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
Thread.currentThread().interrupt() Thread.currentThread().interrupt()
@ -840,4 +891,111 @@ class AzureAsrHelper(private val context: Context) {
*/ */
fun onError(error: String) 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
}
}
}
} }

821
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.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType import com.deep_voice.speech.tts.TtsEventType
import com.yunqiinnovation.ble_service.BleService
/** AzureSpeechPlugin */ /** AzureSpeechPlugin */
class AzureSpeechPlugin: FlutterPlugin { class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
private val tag = "AzureSpeechPlugin" private val tag = "AzureSpeechPlugin"
private lateinit var context: Context private lateinit var context: Context
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
// ASR相关 // ASR相关
private lateinit var asrChannel : MethodChannel private lateinit var asrChannel: MethodChannel
private lateinit var asrEventChannel: EventChannel private lateinit var asrEventChannel: EventChannel
private var asrEventSink: EventChannel.EventSink? = null private var asrEventSink: EventChannel.EventSink? = null
private lateinit var azureAsrHelper: AzureAsrHelper private lateinit var azureAsrHelper: AzureAsrHelper
// TTS相关 // TTS相关
private lateinit var ttsChannel : MethodChannel private lateinit var ttsChannel: MethodChannel
private lateinit var ttsEventChannel: EventChannel private lateinit var ttsEventChannel: EventChannel
private var ttsEventSink: EventChannel.EventSink? = null private var ttsEventSink: EventChannel.EventSink? = null
private lateinit var azureTtsHelper: AzureTtsHelper private lateinit var azureTtsHelper: AzureTtsHelper
// 是否已添加TTS事件监听器
private var isTtsListenerAdded = false // 是否已添加TTS事件监听器
private var isTtsListenerAdded = false
// ASR 事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) { // 是否已注册BLE回调
if (asrEventSink == null) { private var isRegisteredToBle = false
FileLogger.w(tag, "无法发送ASR事件:事件通道未准备好")
return // ASR 事件发送方法
} private fun sendAsrEvent(event: Map<String, Any>) {
if (asrEventSink == null) {
FileLogger.w(tag, "无法发送ASR事件:事件通道未准备好")
return
}
mainHandler.post { mainHandler.post {
try { try {
asrEventSink?.success(event) asrEventSink?.success(event)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "发送ASR事件失败: ${e.message}") FileLogger.e(tag, "发送ASR事件失败: ${e.message}")
} }
}
} }
}
// TTS 事件发送方法 // TTS 事件发送方法
private fun sendTtsEvent(event: Map<String, Any>) { private fun sendTtsEvent(event: Map<String, Any>) {
if (ttsEventSink == null) { if (ttsEventSink == null) {
FileLogger.w(tag, "无法发送TTS事件:事件通道未准备好") FileLogger.w(tag, "无法发送TTS事件:事件通道未准备好")
return return
} }
// 确保在主线程上发送事件 // 确保在主线程上发送事件
mainHandler.post { mainHandler.post {
try { try {
ttsEventSink?.success(event) ttsEventSink?.success(event)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "发送TTS事件失败: ${e.message}") 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"] ?: "未知错误")
)
}
else -> mapOf(
"type" to event.type.toString().lowercase(),
"params" to event.params
)
}
// 发送事件
sendTtsEvent(eventMap)
} }
})
isTtsListenerAdded = true
} }
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
// ASR方法处理器 context = flutterPluginBinding.applicationContext
inner class AsrMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { // 初始化ASR通道
when (call.method) { asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr")
"initialize" -> { asrChannel.setMethodCallHandler(AsrMethodHandler())
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: "" // 初始化TTS通道
val supportedLanguages = call.argument<List<String>>("supportedLanguages") ?: listOf("zh-CN") ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/tts")
val useExternalAudio = call.argument<Boolean>("useExternalAudio") ?: false ttsChannel.setMethodCallHandler(TtsMethodHandler())
try { // 初始化ASR事件通道
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") // asrEventChannel =
// 选择音频源类型 EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr_events")
val audioSourceType = if (useExternalAudio) { asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
AzureAsrHelper.AudioSourceType.EXTERNAL override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
} else { asrEventSink = events
AzureAsrHelper.AudioSourceType.MICROPHONE
} }
val success = azureAsrHelper.initialize( override fun onCancel(arguments: Any?) {
subscriptionKey = subscriptionKey, asrEventSink = null
region = region, }
supportedLanguages = supportedLanguages.toTypedArray(), })
audioSourceType = audioSourceType,
) // 初始化TTS事件通道
ttsEventChannel =
result.success(success) EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/tts_events")
} catch (e: Exception) { ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
result.error("INITIALIZATION_ERROR", e.message, null) override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
} ttsEventSink = events
} setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器
"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 { override fun onCancel(arguments: Any?) {
FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") // ttsEventSink = null
// 选择音频源类型 }
val audioSourceType = if (useExternalAudio) { })
AzureAsrHelper.AudioSourceType.EXTERNAL
// 初始化Azure语音服务
azureTtsHelper = AzureTtsHelper(context)
azureAsrHelper = AzureAsrHelper(context)
// 2. 初始化BleService并注册回调
if (BleService.initialize(context)) {
if (BleService.addCallback(this)) {
isRegisteredToBle = true
FileLogger.d(tag, "已注册BLE回调")
} else { } else {
AzureAsrHelper.AudioSourceType.MICROPHONE FileLogger.w(tag, "BLE回调注册失败")
} }
val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { } else {
override fun onResult(text: String, detectedLanguage: String) { FileLogger.e(tag, "BLE服务初始化失败")
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) * 设置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
} }
else -> { }
result.notImplemented()
// 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方法处理器
// TTS方法处理器 inner class TtsMethodHandler : MethodCallHandler {
inner class TtsMethodHandler : MethodCallHandler { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) {
when (call.method) { "initialize" -> {
"initialize" -> { val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val subscriptionKey = call.argument<String>("subscriptionKey") ?: "" val region = call.argument<String>("region") ?: ""
val region = call.argument<String>("region") ?: "" val useCustomAudioOutput =
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false call.argument<Boolean>("useCustomAudioOutput") ?: false
try { try {
// 初始化TTS引擎(使用新的接口方法) // 初始化TTS引擎(使用新的接口方法)
val success = azureTtsHelper.initialize( val success = azureTtsHelper.initialize(
ttsAppId = "", // Azure TTS不需要appId ttsAppId = "", // Azure TTS不需要appId
ttsAppToken = subscriptionKey, ttsAppToken = subscriptionKey,
ttsResource = region, ttsResource = region,
language = "zh-CN" language = "zh-CN"
) )
// 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器 // 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器
if (success && useCustomAudioOutput) { if (success && useCustomAudioOutput) {
// 创建SimpleAudioPlayer实例 // 创建SimpleAudioPlayer实例
val customPlayer = SimpleAudioPlayer(context) val customPlayer = SimpleAudioPlayer(context)
// 获取并设置音频输出流 // 获取并设置音频输出流
val outputStream = customPlayer.getAudioOutputStream() val outputStream = customPlayer.getAudioOutputStream()
if (outputStream != null) { if (outputStream != null) {
azureTtsHelper.setCustomAudioOutputStream(outputStream) 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()
}
} }
}
}
// 确保设置了TTS事件监听器 override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
setupTtsEventListener() 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()
result.success(success) } catch (e: Exception) {
} catch (e: Exception) { FileLogger.e(tag, "释放资源失败: ${e.message}")
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" -> { // BleService.Callback 接口实现
azureTtsHelper.release() override fun onScanResult(devices: List<Map<String, Any>>) {
result.success(true) // 不处理扫描结果
} }
else -> {
result.notImplemented() override fun onConnectionStateChanged(state: Int) {
}
override fun onAudioDataReceived(data: ByteArray) {
if (azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) {
azureAsrHelper.pushAudioData(data)
} }
} // 可选:处理音频数据
}
/**
* 处理唤醒信号
* 在收到唤醒信号时启动语音识别
*/
override fun onWakeupSignalReceived() {
} }
}
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
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}")
} }
}
} }

3
local_plugins/azure_speech/pubspec.yaml

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

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

@ -1,38 +1,18 @@
package com.yunqiinnovation.ble_service package com.yunqiinnovation.ble_service
import android.Manifest
import android.app.PendingIntent
import android.bluetooth.* import android.bluetooth.*
import android.bluetooth.le.* import android.bluetooth.le.*
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.* import android.os.*
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import android.util.Log import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.* 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 java.util.concurrent.CopyOnWriteArrayList
// Import Jieli Opus SDK classes // Import Jieli Opus SDK classes
import com.jieli.jl_audio_decode.opus.model.OpusOption import com.jieli.jl_audio_decode.opus.model.OpusOption
import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback 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.opus.OpusManager
import com.jieli.jl_audio_decode.exceptions.OpusException 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服务类:提供蓝牙低功耗设备的扫描、连接和通信功能 * BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能

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.bluetooth.BluetoothDevice
import android.content.Context import android.content.Context
import com.jieli.otasdk.tool.ota.OTAManager 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.constant.StateCode
import com.jieli.jl_bt_ota.interfaces.BtEventCallback import com.jieli.jl_bt_ota.interfaces.BtEventCallback
import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback
import com.jieli.jl_bt_ota.model.base.BaseError 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 com.jieli.otasdk.tool.bluetooth.BluetoothHelper
import androidx.lifecycle.MutableLiveData
import com.jieli.otasdk.model.DeviceConnection
import android.util.Log import android.util.Log
import androidx.annotation.NonNull
import com.jieli.otasdk.tool.config.ConfigHelper import com.jieli.otasdk.tool.config.ConfigHelper
import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst import com.yunqiinnovation.ble_service.BleConst
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import java.io.File
import java.io.FileOutputStream
import java.io.RandomAccessFile
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
class CustomUpdateCallback( class CustomUpdateCallback(
@ -130,7 +119,10 @@ class CustomUpdateCallback(
Log.d(TAG, "升级失败: ${error?.message ?: "未知错误"}") Log.d(TAG, "升级失败: ${error?.message ?: "未知错误"}")
if (streamSink != null) { if (streamSink != null) {
if (Looper.myLooper() == Looper.getMainLooper()) { 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( streamSink.success(
mapOf( mapOf(
"event" to "onError", "event" to "onError",
@ -282,8 +274,20 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
bluetoothHelper = BluetoothHelper(context) bluetoothHelper = BluetoothHelper(context)
// 如果需要额外的初始化逻辑,请在这里添加 // 如果需要额外的初始化逻辑,请在这里添加
otaManager.registerBluetoothCallback(bluetoothCallback) otaManager.registerBluetoothCallback(bluetoothCallback)
BleService.addCallback(this) // 初始化BleService
isRegisteredToBle = true // 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") Log.i("TAG", "onAttachedToEngine")
} }
@ -309,6 +313,9 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
otaDevice = bluetoothGatt.device otaDevice = bluetoothGatt.device
BleService.disconnect() BleService.disconnect()
} else if (otaDevice != null) {
isDisconnect = true
BleService.disconnect()
} else { } else {
Log.e("TAG", "BluetoothGatt 或 device 为 null,无法执行操作") Log.e("TAG", "BluetoothGatt 或 device 为 null,无法执行操作")
sendEventOnMainThread( sendEventOnMainThread(
@ -321,9 +328,11 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
} }
} else if (call.method == "onOtaClose") { } else if (call.method == "onOtaClose") {
BleService.isEnterOta = false BleService.isEnterOta = false
isDisconnect = false isDisconnect = false
otaManager.bluetoothHelper.stopScan()
otaManager.bluetoothHelper.disconnectDevice(otaDevice)
} else { } else {
result.notImplemented() result.notImplemented()
} }
@ -345,6 +354,7 @@ class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler {
} }
// BleService.Callback 接口实现
override fun onScanResult(devices: List<Map<String, Any>>) { 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 { } else {
Log.e(TAG, "连接设备失败: ${device.name} (${device.address})"); Log.e(TAG, "连接设备失败: ${device.name} (${device.address})");
} }
}
mDiscoveredBleDevices.add(device); mDiscoveredBleDevices.add(device);
handleDiscoveryBle(device, new BleScanInfo().setRawData(scanRecord).setRssi(rssi).setEnableConnect(isBleEnableConnect)); 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:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart';
class OtaEvent { class OtaEvent {
static final int NOT_STARTED = -1; // 未开始 static final int NOT_STARTED = -1; // 未开始
@ -57,7 +56,7 @@ class Ota {
// 私有构造函数 // 私有构造函数
Ota._internal() { Ota._internal() {
// 初始化时设置事件监听 // 初始化时设置事件监听
_setupEventListener(); // _setupEventListener();
} }
// 公共访问点 // 公共访问点
@ -66,13 +65,13 @@ class Ota {
static const EventChannel _eventChannel = EventChannel( static const EventChannel _eventChannel = EventChannel(
'com.example.ota/events', 'com.example.ota/events',
); );
final RxDouble progress = 0.0.obs; // double progress = 0.0;
final RxInt type = OtaEvent.NOT_STARTED.obs; // late int type = OtaEvent.NOT_STARTED;
final RxBool isUpgrade = false.obs; // bool isUpgrade = false;
/// 事件流 /// 事件流
Stream<OtaEvent>? _eventStream; Stream<OtaEvent>? _eventStream;
bool _isEventListenerSet = false; // 防止重复监听 // bool _isEventListenerSet = false; // 防止重复监听
/// 获取事件流 /// 获取事件流
Stream<OtaEvent> get eventStream { Stream<OtaEvent> get eventStream {
@ -90,53 +89,48 @@ class Ota {
onOtaClose(); onOtaClose();
// 停止事件监听 // 停止事件监听
_eventStream = null; _eventStream = null;
_isEventListenerSet = false; //_isEventListenerSet = false;
// 清理 Rx 对象
progress.close();
type.close();
isUpgrade.value = false;
print("Ota 对象资源已释放"); print("Ota 对象资源已释放");
} }
/// 内部设置事件监听(只执行一次) // /// 内部设置事件监听(只执行一次)
void _setupEventListener() { // void _setupEventListener() {
if (_isEventListenerSet) return; // if (_isEventListenerSet) return;
eventStream.listen((event) { // eventStream.listen((event) {
if (event.event == 'onProgress' && // if (event.event == 'onProgress' &&
event.type != null && // event.type != null &&
event.progress != null) { // event.progress != null) {
print("flutter 收到数据${event.progress}type=${type.value}"); // print("flutter 收到数据${event.progress}type=${type}");
progress.value = event.progress!; // progress = event.progress!;
type.value = event.type!; // type = event.type!;
} // }
if (event.event == 'onStartOTA') { // if (event.event == 'onStartOTA') {
type.value = OtaEvent.START_UPGRADING; // type = OtaEvent.START_UPGRADING;
isUpgrade.value = true; // 开始升级 // isUpgrade = true; // 开始升级
} // }
if (event.event == 'onStopOTA') { // if (event.event == 'onStopOTA') {
type.value = OtaEvent.UPGRADE_SUCCESS; // type = OtaEvent.UPGRADE_SUCCESS;
isUpgrade.value = false; // isUpgrade = false;
} // }
if (event.event == 'onError' && // if (event.event == 'onError' &&
event.code != null && // event.code != null &&
event.message != null) { // event.message != null) {
print("flutter onError${event.code}type=${event.message}"); // print("flutter onError${event.code}type=${event.message}");
type.value = OtaEvent.UPGRADE_FAILED; // type = OtaEvent.UPGRADE_FAILED;
isUpgrade.value = false; // isUpgrade = false;
} // }
if (event.event == 'NOCONNECTION') { // if (event.event == 'NOCONNECTION') {
type.value = OtaEvent.CONNECTION_DISCONNECTION; // type = OtaEvent.CONNECTION_DISCONNECTION;
isUpgrade.value = false; // isUpgrade = false;
} // }
if (event.event == 'CONNECTION_SUCCESS') { // if (event.event == 'CONNECTION_SUCCESS') {
type.value = OtaEvent.CONNECTION_SUCCESS; // type = OtaEvent.CONNECTION_SUCCESS;
} // }
}); // });
_isEventListenerSet = true; // _isEventListenerSet = true;
} // }
Future<String?> startUpgrading(String? filePath) { Future<String?> startUpgrading(String? filePath) {
return OtaPlatform.instance.startUpgrading(filePath); return OtaPlatform.instance.startUpgrading(filePath);

1
local_plugins/ota/pubspec.yaml

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

2
pubspec.yaml

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

Loading…
Cancel
Save