diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a2c7bb84e..3d1ae458c 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -11,6 +11,8 @@
+
+
diff --git a/lib/modules/permissions/views/permissions_view.dart b/lib/modules/permissions/views/permissions_view.dart
index 6e633ae81..eca8a7d65 100644
--- a/lib/modules/permissions/views/permissions_view.dart
+++ b/lib/modules/permissions/views/permissions_view.dart
@@ -69,6 +69,15 @@ class PermissionsView extends GetView {
final groups = snapshot.data ?? [];
final List widgets = [];
+ // 添加后台权限条目到最前端(仅在满足条件时)
+ final backgroundPermissionWidget = _buildBackgroundPermissionSection(isDarkMode);
+ if (backgroundPermissionWidget is! SizedBox) {
+ widgets.add(backgroundPermissionWidget);
+ if (groups.isNotEmpty) {
+ widgets.add(SizedBox(height: 20.h));
+ }
+ }
+
for (int i = 0; i < groups.length; i++) {
final group = groups[i];
@@ -284,6 +293,285 @@ class PermissionsView extends GetView {
);
}
+ /// 构建后台权限条目
+ Widget _buildBackgroundPermissionSection(bool isDarkMode) {
+ // 检查是否为Android系统且语言为中文
+ if (!_shouldShowBackgroundPermissionTutorial()) {
+ return const SizedBox.shrink(); // 不满足条件时完全隐藏
+ }
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // 组标题
+ Padding(
+ padding: EdgeInsets.only(left: 4.w, bottom: 8.h),
+ child: Text(
+ '后台权限'.tr, // 后台权限
+ style: TextStyle(
+ fontSize: 12.sp,
+ fontWeight: FontWeight.w500,
+ color: isDarkMode ? Colors.blue[400] : Colors.blue[600],
+ letterSpacing: 1,
+ ),
+ ),
+ ),
+
+ // 权限项容器
+ Container(
+ decoration: BoxDecoration(
+ color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
+ borderRadius: BorderRadius.circular(16.r),
+ boxShadow: [
+ BoxShadow(
+ color: isDarkMode
+ ? Colors.black.withOpacity(0.2)
+ : Colors.black.withOpacity(0.05),
+ blurRadius: 10,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(16.r),
+ child: BackdropFilter(
+ filter: isDarkMode
+ ? ImageFilter.blur(sigmaX: 5, sigmaY: 5)
+ : ImageFilter.blur(sigmaX: 0, sigmaY: 0),
+ child: _buildBackgroundPermissionItem(isDarkMode),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ /// 检查是否应该显示后台权限教程入口
+ /// 仅在Android系统且中文环境下显示
+ bool _shouldShowBackgroundPermissionTutorial() {
+ // 1. 检查是否为Android系统
+ if (!Platform.isAndroid) {
+ return false;
+ }
+
+ // 2. 检查设备语言区域是否为中文
+ final locale = Get.locale ?? Get.deviceLocale;
+ if (locale == null) {
+ return false;
+ }
+
+ // 检查语言代码是否为中文(zh)
+ return locale.languageCode == 'zh';
+ }
+
+ /// 构建后台权限条目内容
+ Widget _buildBackgroundPermissionItem(bool isDarkMode) {
+ return InkWell(
+ onTap: () {
+ _showBackgroundPermissionGuide();
+ },
+ child: Padding(
+ padding: EdgeInsets.all(16.w),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '后台运行'.tr, // 确保随时可以唤醒AI助手
+ style: TextStyle(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w500,
+ color: isDarkMode ? Colors.white : Colors.black87,
+ ),
+ ),
+ SizedBox(height: 4.h),
+ Text(
+ '设置后台运行权限,确保AI助手随时响应您的需求'.tr, // 设置后台运行权限,确保AI助手随时响应您的需求
+ style: TextStyle(
+ fontSize: 12.sp,
+ color: isDarkMode ? Colors.white70 : Colors.grey[600],
+ ),
+ ),
+ ],
+ ),
+ ),
+ Icon(
+ Icons.arrow_forward_ios,
+ size: 16.sp,
+ color: isDarkMode ? Colors.white70 : Colors.grey[600],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ // 显示后台权限设置指南
+ void _showBackgroundPermissionGuide() {
+ final isDarkMode = Theme.of(Get.context!).brightness == Brightness.dark;
+
+ Get.bottomSheet(
+ Container(
+ height: Get.height * 0.8,
+ decoration: BoxDecoration(
+ color: isDarkMode ? Colors.grey[900] : Colors.white,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)),
+ ),
+ child: Column(
+ children: [
+ // 顶部标题栏
+ Container(
+ padding: EdgeInsets.all(16.w),
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: isDarkMode
+ ? Colors.grey[700]!
+ : Colors.grey.shade300)),
+ ),
+ child: Row(
+ children: [
+ Text(
+ '后台运行权限设置'.tr,
+ style: TextStyle(
+ fontSize: 18.sp,
+ fontWeight: FontWeight.bold,
+ color: isDarkMode ? Colors.white : Colors.black,
+ ),
+ ),
+ Spacer(),
+ IconButton(
+ onPressed: () => Get.back(),
+ icon: Icon(
+ Icons.close,
+ color: isDarkMode ? Colors.white : Colors.black,
+ ),
+ ),
+ ],
+ ),
+ ),
+ // 内容区域
+ Expanded(
+ child: SingleChildScrollView(
+ padding: EdgeInsets.all(16.w),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ '为确保AI助手能够在后台正常运行,请根据您的手机品牌进行以下设置:'.tr,
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: isDarkMode ? Colors.grey[300] : Colors.grey[600],
+ ),
+ ),
+ SizedBox(height: 20.h),
+ _buildBrandGuide(
+ '华为/荣耀',
+ [
+ '设置 → 应用 → 应用启动管理',
+ '找到Voitrans应用',
+ '关闭"自动管理"',
+ '开启"允许自启动"、"允许关联启动"、"允许后台活动"'
+ ],
+ isDarkMode),
+ _buildBrandGuide(
+ 'OPPO',
+ ['设置 → 应用 → 自启动', '找到Voitrans应用', '打开"后台自启动"'],
+ isDarkMode),
+ _buildBrandGuide(
+ 'vivo',
+ ['i管家 → 应用管理 → 自启动 → 关联启动', '找到Voitrans应用', '允许"关联启动"'],
+ isDarkMode),
+ _buildBrandGuide(
+ '小米',
+ ['设置 → 应用设置 → 应用管理', '找到Voitrans应用', '打开"自启动"'],
+ isDarkMode),
+ _buildBrandGuide(
+ '三星',
+ ['长按Voitrans图标', '应用程序信息 → 电池', '打开"允许后台活动"'],
+ isDarkMode),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ isScrollControlled: true,
+ );
+ }
+
+ // 构建品牌设置指南
+ Widget _buildBrandGuide(String brand, List steps, bool isDarkMode) {
+ return Container(
+ margin: EdgeInsets.only(bottom: 20.h),
+ padding: EdgeInsets.all(16.w),
+ decoration: BoxDecoration(
+ color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[50],
+ borderRadius: BorderRadius.circular(12.r),
+ border: Border.all(
+ color: isDarkMode ? Colors.grey[700]! : Colors.grey.shade300),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ brand,
+ style: TextStyle(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.bold,
+ color: isDarkMode ? Colors.blue[400] : Colors.blue[600],
+ ),
+ ),
+ SizedBox(height: 8.h),
+ ...steps.asMap().entries.map((entry) {
+ int index = entry.key;
+ String step = entry.value;
+ return Padding(
+ padding: EdgeInsets.only(bottom: 4.h),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ width: 20.w,
+ height: 20.w,
+ decoration: BoxDecoration(
+ color: isDarkMode ? Colors.blue[400] : Colors.blue[600],
+ shape: BoxShape.circle,
+ ),
+ child: Center(
+ child: Text(
+ '${index + 1}',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12.sp,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ SizedBox(width: 8.w),
+ Expanded(
+ child: Text(
+ step,
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: isDarkMode ? Colors.white : Colors.black87,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }).toList(),
+ ],
+ ),
+ );
+ }
+
void _handlePermissionToggle(
Permission permission,
PermissionStatus status,
diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt
index ee4e9fed0..a7e202690 100644
--- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt
+++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt
@@ -201,7 +201,7 @@ class SimpleAudioReceiver(private val context: Context) {
fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallback?) {
try {
this.audioSourceType = audioSourceType
- this.audioDataCallback = callback
+ this.audioDataCallback = callback
isWriting.set(true)
@@ -209,10 +209,6 @@ fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallba
Log.d("tag", "存下来的数据: ${writeQueue?.size}")
}
- // 启动前台服务
- AudioRecordingForegroundService.startService(context)
- Log.d(TAG, "前台服务已启动,开始音频录制")
-
when (audioSourceType) {
AudioSourceType.MICROPHONE -> runMicrophoneCapture()
AudioSourceType.EXTERNAL -> runExternalCapture()
@@ -287,6 +283,9 @@ fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallba
*/
private fun runMicrophoneCapture() {
try {
+ // 启动前台服务
+ AudioRecordingForegroundService.startService(context)
+ Log.d(TAG, "前台服务已启动,开始音频录制")
// 根据手机品牌决定是否设置通话音频模式
if (isHonorDevice()) {
Log.d(TAG, "检测到荣耀设备,启用通话音频模式")
diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
index 2b1245106..87c242ef8 100644
--- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
+++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
@@ -249,7 +249,10 @@ private var cmdReplyType: UInt8 = 0
clearPairedMacAddress()
}
/// 开始扫描BLE设备
- public func startScan(timeout: TimeInterval = 30.0) -> Bool {
+ /// @param timeout 扫描超时时间,默认30秒
+ /// @param uuids 要扫描的服务UUID列表
+ /// @return 是否成功开始扫描
+ public func startScan(timeout: TimeInterval = 30.0, uuids: [String]) -> Bool {
if centralManager == nil {
initializeCentralManager()
}
@@ -269,16 +272,43 @@ private var cmdReplyType: UInt8 = 0
isScanning = true
- // // 移除服务UUID过滤,扫描所有设备
- // manager.scanForPeripherals(withServices: nil, options: [
+ // 构建服务UUID数组进行过滤
+ var serviceUUIDs: [CBUUID] = []
+
+ // // 添加默认的服务UUID
+ // serviceUUIDs.append(BleConst.SERVICE_UUID)
+ // serviceUUIDs.append(BleConst.SERVICE_UUID1)
+
+ // 处理传入的UUID参数
+ for uuidStr in uuids {
+ do {
+ let cbuuid: CBUUID
+ if uuidStr.count == 4 {
+ // 短UUID,直接使用
+ cbuuid = CBUUID(string: uuidStr)
+ } else if uuidStr.count == 36 {
+ // 完整UUID
+ cbuuid = CBUUID(string: uuidStr)
+ } else {
+ os_log("⚠️ 无效的UUID格式: %{public}@", log: logger, type: .default, uuidStr)
+ continue
+ }
+ serviceUUIDs.append(cbuuid)
+ os_log("✅ 添加扫描过滤器UUID: %{public}@", log: logger, type: .info, uuidStr)
+ } catch {
+ os_log("❌ 构造UUID失败: %{public}@, 错误: %{public}@", log: logger, type: .error, uuidStr, error.localizedDescription)
+ }
+ }
+
+ // // 开始扫描,使用构建的服务UUID数组进行过滤
+ // manager.scanForPeripherals(withServices: serviceUUIDs.isEmpty ? nil : serviceUUIDs, options: [
// CBCentralManagerScanOptionAllowDuplicatesKey: true // 允许重复以获取更新的广播数据
// ])
- let serviceUUIDs = [BleConst.SERVICE_UUID, BleConst.SERVICE_UUID1]
-
- manager.scanForPeripherals(withServices: nil, options: [
+ manager.scanForPeripherals(withServices: nil , options: [
CBCentralManagerScanOptionAllowDuplicatesKey: true // 允许重复以获取更新的广播数据
])
- os_log("🔍 开始扫描所有BLE设备(无过滤),超时时间: %.1f秒", log: logger, type: .info, timeout)
+ let filterInfo = serviceUUIDs.isEmpty ? "无过滤" : "过滤器数量: \(serviceUUIDs.count)"
+ os_log("🔍 开始扫描BLE设备(%{public}@),超时时间: %.1f秒", log: logger, type: .info, filterInfo, timeout)
// 设置扫描超时
scanTimer?.invalidate()
diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift
index dd9d07f14..eb7fc5215 100644
--- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift
+++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftBleServicePlugin.swift
@@ -112,9 +112,10 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
case "startScan":
let arguments = call.arguments as? [String: Any]
let timeout = (arguments?["timeout"] as? Double) ?? 15.0
+ let uuids = (arguments?["uuids"] as? [String]) ?? []
os_log("🔍 开始传统BLE扫描,超时: %.1f秒", log: logger, type: .info, timeout)
- BleService.shared.startScan(timeout: timeout)
+ BleService.shared.startScan(timeout: timeout, uuids: uuids)
result(true)
case "stopScan":
diff --git a/pubspec.yaml b/pubspec.yaml
index 0346ef700..1a6b7388d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: voitrans
description: "Voitrans - AI Voice Assistant."
publish_to: "none"
-version: 1.0.13+34
+version: 1.0.14+37
environment:
sdk: ">=3.3.0 <4.0.0"