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"