Browse Source

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

newdev_shunjiawei
tanlongsheng 1 year ago
parent
commit
3004e52a81
  1. 8
      GoogleService-Info.plist
  2. 2
      ios/Podfile
  3. 8
      ios/Podfile.lock
  4. 8
      ios/Runner.xcodeproj/project.pbxproj
  5. 10
      ios/Runner/Info.plist
  6. 31
      lib/core/bindings/initial_binding.dart
  7. 286
      lib/modules/pairing/controllers/pairing_controller.dart
  8. 78
      lib/modules/settings/controllers/settings_controller.dart
  9. 324
      lib/modules/settings/views/permissions_view.dart
  10. 92
      lib/modules/settings/views/settings_view.dart
  11. 8
      lib/routes/app_pages.dart
  12. 1
      lib/routes/app_routes.dart
  13. 19
      local_plugins/ble_service/LICENSE
  14. 15
      local_plugins/ble_service/ios/Assets/ble_service-Info.plist
  15. 195
      local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift
  16. 75
      local_plugins/ble_service/ios/Classes/BleConst.swift
  17. 25
      local_plugins/ble_service/ios/Classes/BleDelegate.swift
  18. 1067
      local_plugins/ble_service/ios/Classes/BleService.swift
  19. 2
      local_plugins/ble_service/ios/Classes/BleServicePlugin.h
  20. 360
      local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift
  21. 87
      local_plugins/ble_service/ios/README.md
  22. 10
      local_plugins/ble_service/ios/ble_service.podspec
  23. 8
      local_plugins/ble_service/lib/ble_service.dart
  24. 4
      local_plugins/ble_service/pubspec.yaml
  25. 304
      local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift
  26. 136
      local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift
  27. 42
      local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift
  28. 2
      local_plugins/open_ai_service/android/src/main/AndroidManifest.xml
  29. 421
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt
  30. BIN
      local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock
  31. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  32. 2
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties
  33. 45
      test.md

8
GoogleService-Info.plist

@ -3,9 +3,9 @@
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com</string>
<string>907754492821-tl2blr39g82re40sttkve36ovt6qcu6c.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278</string>
<string>com.googleusercontent.apps.907754492821-tl2blr39g82re40sttkve36ovt6qcu6c</string>
<key>ANDROID_CLIENT_ID</key>
<string>907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com</string>
<key>API_KEY</key>
@ -15,7 +15,7 @@
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.yunqiinnovation.deepsound</string>
<string>com.chuangxiangyunqi.deapsound</string>
<key>PROJECT_ID</key>
<string>deepsound-e0e3d</string>
<key>STORAGE_BUCKET</key>
@ -31,6 +31,6 @@
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:907754492821:ios:246794fc44b23174145c9c</string>
<string>1:907754492821:ios:e4b4f3b37e58a7d1145c9c</string>
</dict>
</plist>

2
ios/Podfile

@ -1,5 +1,5 @@
# Define a global platform for your project
platform :ios, '13.0'
platform :ios, '18.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

8
ios/Podfile.lock

@ -11,6 +11,8 @@ PODS:
- Flutter
- audio_waveforms (0.0.1):
- Flutter
- ble_service (0.0.1):
- Flutter
- chat_storage (0.0.1):
- Flutter
- classic_bluetooth (0.0.1):
@ -231,6 +233,7 @@ DEPENDENCIES:
- app_settings (from `.symlinks/plugins/app_settings/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- ble_service (from `.symlinks/plugins/ble_service/ios`)
- chat_storage (from `.symlinks/plugins/chat_storage/ios`)
- classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@ -302,6 +305,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/audio_session/ios"
audio_waveforms:
:path: ".symlinks/plugins/audio_waveforms/ios"
ble_service:
:path: ".symlinks/plugins/ble_service/ios"
chat_storage:
:path: ".symlinks/plugins/chat_storage/ios"
classic_bluetooth:
@ -370,6 +375,7 @@ SPEC CHECKSUMS:
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
ble_service: 068ebe51800424f980282b16cbf757574eec4e08
chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8
classic_bluetooth: c5e5c0c86acab8dcd6b8e9cef7fca654c3518034
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
@ -430,6 +436,6 @@ SPEC CHECKSUMS:
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
PODFILE CHECKSUM: 017ca79b199d8f37c4acb8d83ec912447003b920
PODFILE CHECKSUM: f62382080fcf32f2d4640dc419aa07d69eff0328
COCOAPODS: 1.16.2

8
ios/Runner.xcodeproj/project.pbxproj

@ -44,6 +44,7 @@
/* Begin PBXFileReference section */
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
@ -106,6 +107,7 @@
1E5DDD8701F6FB27AA502080 /* Frameworks */ = {
isa = PBXGroup;
children = (
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */,
E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */,
F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */,
);
@ -499,6 +501,7 @@
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -519,6 +522,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -537,6 +541,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -553,6 +558,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -685,6 +691,7 @@
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -709,6 +716,7 @@
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

10
ios/Runner/Info.plist

@ -41,6 +41,12 @@
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
<string>external-accessory</string>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixin</string>
@ -108,5 +114,9 @@
<true/>
<key>FacebookAutoLogAppEventsEnabled</key>
<true/>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
</dict>
</plist>

31
lib/core/bindings/initial_binding.dart

@ -4,9 +4,7 @@ import '../../data/services/speech_factory.dart';
import '../../data/services/chat_history_service.dart';
import '../../data/services/volcano_translation_service.dart';
import '../../data/services/language_manager.dart';
import '../../data/services/volcano_ai_service.dart';
import '../../core/utils/logger.dart';
import '../../data/services/open_ai_service_adapter.dart';
import '../../data/services/bluetooth_manager.dart';
import '../../data/services/ble_manager.dart';
@ -17,10 +15,11 @@ class InitialBinding extends Bindings {
Logger.warning('InitialBinding dependencies');
// 语言管理器(需要最先初始化)
Get.put<LanguageManager>(LanguageManager(), permanent: true);
// 语言管理器(需要最先初始化)
Get.lazyPut<LanguageManager>(() => LanguageManager(), fenix: true);
//初始化语音工厂
Get.put<SpeechFactory>(SpeechFactory(), permanent: true);
Get.lazyPut<SpeechFactory>(() => SpeechFactory(), fenix: true);
Get.find<SpeechFactory>().initialize(initialType: SpeechServiceType.azure);
// 历史记录服务
@ -30,30 +29,14 @@ class InitialBinding extends Bindings {
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(),
fenix: true);
// OpenAI服务适配器
Get.lazyPut<OpenAIServiceAdapter>(() {
final adapter = OpenAIServiceAdapter();
adapter.initialize();
return adapter;
}, fenix: true);
// 蓝牙服务
Get.put<BluetoothManager>(BluetoothManager(), permanent: true);
Get.lazyPut<BluetoothManager>(() => BluetoothManager(), fenix: true);
// BleManager
Get.put<BleManager>(BleManager(), permanent: true);
Get.lazyPut<BleManager>(() => BleManager(), fenix: true);
// MusicManager
Get.put<MusicManager>(MusicManager(), permanent: true);
Get.lazyPut<MusicManager>(() => MusicManager(), fenix: true);
// 火山AI服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 蓝牙媒体按钮服务
// Get.lazyPut<BluetoothMediaButtonService>(
// () => BluetoothMediaButtonService(),
// fenix: true);
// // 语音交互服务
// Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),
// fenix: true);
// Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 杰理蓝牙服务

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

@ -15,8 +15,6 @@ import 'package:flutter/material.dart';
class PairingController extends GetxController {
static PairingController get to => Get.find();
// 需要设备
final bool needdeivice = false;
// 状态标志
final isLoading = false.obs;
@ -65,6 +63,9 @@ class PairingController extends GetxController {
// BLE服务
final _bleService = BleService();
// 日志tag
static const _tag = 'PairingController';
@override
void onInit() {
super.onInit();
@ -102,7 +103,7 @@ class PairingController extends GetxController {
// 检查蓝牙状态并刷新设备
await _checkBluetoothAndRefresh();
} catch (e) {
Logger.error('初始化配对控制器失败: ${e.toString()}');
Logger.e(_tag, '初始化配对控制器失败: ${e.toString()}');
statusDescription.value = 'initializationFailure'.tr;
} finally {
isLoading.value = false;
@ -134,13 +135,12 @@ class PairingController extends GetxController {
/// 检查设备配对状态
Future<void> _checkDevicePairingStatus() async {
// 只在Android平台且设备已连接时执行
if (!Platform.isAndroid || !isDeviceConnected) return;
if (!isDeviceConnected) return;
try {
final isSupported = await _bleService.isCompanionSupported();
if (!isSupported) {
Logger.info('设备不支持Companion功能');
Logger.i(_tag, '设备不支持Companion功能');
_companionAssociated.value = true; // 不支持则视为已配对
_userRejectedAssociation.value = false; // 重置拒绝状态
return;
@ -149,16 +149,16 @@ class PairingController extends GetxController {
final pairedMac = await _bleService.getPairedMacAddress();
if (pairedMac == null) {
// 没有配对记录,需要开始配对
Logger.info('无配对记录,需要开始设备配对');
Logger.i(_tag, '无配对记录,需要开始设备配对');
return;
}
Logger.info('发现已关联的Companion设备: $pairedMac');
Logger.i(_tag, '发现已关联的Companion设备: $pairedMac');
// 检查当前连接设备与已保存的设备是否一致
if (currentDevice!.address != pairedMac) {
// MAC地址不匹配
Logger.warning('当前连接的设备与已配对的DeepSound设备不符');
Logger.w(_tag, '当前连接的设备与已配对的DeepSound设备不符');
statusDescription.value = 'pleaseConnectPairedDevice'.tr;
return;
}
@ -170,13 +170,13 @@ class PairingController extends GetxController {
}
// 设备匹配且合规,标记为已配对
Logger.info('当前设备与已配对设备匹配');
Logger.i(_tag, '当前设备与已配对设备匹配');
_companionAssociated.value = true;
_userRejectedAssociation.value = false; // 重置拒绝状态
await _bleService.registerBackgroundScan();
_autoNavigateToHome();
} catch (e) {
Logger.error('检查设备配对状态失败: ${e.toString()}');
Logger.e(_tag, '检查设备配对状态失败: ${e.toString()}');
}
}
@ -198,45 +198,70 @@ class PairingController extends GetxController {
permissionsToRequest.add(Permission.bluetoothConnect);
}
} else if (Platform.isIOS) {
// iOS上单独处理蓝牙权限请求,避免冲突
PermissionStatus status = await Permission.bluetooth.status;
// iOS需要请求的权限
permissionsToRequest.add(Permission.bluetooth);
permissionsToRequest.add(Permission.location);
// 如果需要麦克风权限(例如语音功能)
// permissionsToRequest.add(Permission.microphone);
if (status.isPermanentlyDenied) {
Logger.warning('iOS蓝牙权限状态为permanentlyDenied,这可能是iOS系统误报');
try {
// 先尝试请求权限
if (permissionsToRequest.isNotEmpty) {
statuses = await permissionsToRequest.request();
bool allGranted = true;
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
Logger.w(_tag, 'iOS权限 ${permission.toString()} 未被授予: $status');
// 特殊处理蓝牙权限
if (permission == Permission.bluetooth) {
Logger.w(_tag, 'iOS蓝牙权限未授予,状态: $status');
}
}
});
// 如果有权限未授予,但不是永久拒绝,继续尝试
if (!allGranted) {
bool hasPermanentlyDenied = statuses.values.any((status) => status.isPermanentlyDenied);
if (hasPermanentlyDenied) {
// 有权限被永久拒绝,需要引导用户去设置
_showPermissionSettingsDialog('部分权限被永久拒绝,请在设置中启用');
return false;
}
}
}
// 在iOS上打开蓝牙设置对话框
Get.dialog(
AlertDialog(
title: Text('需要蓝牙权限'),
content: Text('请在系统设置中手动开启蓝牙权限,以便连接设备'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('稍后'),
),
TextButton(
onPressed: () {
Get.back();
openAppSettings();
},
child: Text('前往设置'),
),
],
),
barrierDismissible: false,
);
return false;
} else if (!status.isGranted) {
// 单独请求蓝牙权限
PermissionStatus result = await Permission.bluetooth.request();
if (!result.isGranted) {
Logger.warning('iOS蓝牙权限申请失败: $result');
return false;
// 直接尝试使用蓝牙功能,这会触发iOS原生权限请求
bool isEnabled = await _bluetoothService.isBluetoothEnabled();
Logger.i(_tag, 'iOS蓝牙状态检查: ${isEnabled ? "已开启" : "未开启"}');
// 检测AccessorySetupKit支持情况(iOS 18+特性)
bool companionSupported = await _bleService.isCompanionSupported();
Logger.i(_tag, 'iOS Companion支持检查: ${companionSupported ? "支持" : "不支持"}');
if (companionSupported) {
// 针对iOS 18+,尝试预热AccessorySetupKit框架
try {
// 此调用会触发AccessorySetupKit相关权限请求
final pairedMac = await _bleService.getPairedMacAddress();
Logger.i(_tag, '获取已配对设备MAC: ${pairedMac ?? "无"}');
} catch (e) {
Logger.w(_tag, 'iOS 18+ AccessorySetupKit初始化失败: ${e.toString()}');
}
}
// 在iOS上视为权限已授予
_hasPermissions.value = true;
return true;
} catch (e) {
Logger.e(_tag, "iOS蓝牙初始化失败: ${e.toString()}");
// 显示自定义对话框引导用户
_showPermissionSettingsDialog('需要开启蓝牙权限才能配对设备');
return false;
}
// iOS不再添加到批量请求列表
// permissionsToRequest.add(Permission.bluetooth);
}
// 如果还有其他权限需要请求
@ -247,7 +272,7 @@ class PairingController extends GetxController {
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
Logger.warning('权限 ${permission.toString()} 未被授予: $status');
Logger.w(_tag, '权限 ${permission.toString()} 未被授予: $status');
}
});
@ -259,6 +284,30 @@ class PairingController extends GetxController {
_hasPermissions.value = true;
return true;
}
/// 显示权限设置对话框
void _showPermissionSettingsDialog(String message) {
Get.dialog(
AlertDialog(
title: Text('需要权限'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('稍后'),
),
TextButton(
onPressed: () {
Get.back();
openBluetoothSettings();
},
child: Text('去设置'),
),
],
),
barrierDismissible: false,
);
}
// 辅助方法获取Android SDK版本
Future<int> _getAndroidSdkVersion() async {
@ -281,14 +330,11 @@ class PairingController extends GetxController {
_deviceWorker =
ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) {
if (device != null && !_isNavigatingToHome) {
Logger.info('配对页面发现设备已连接: ${device.name}');
Logger.i(_tag, '配对页面发现设备已连接: ${device.name}');
_handleDeviceConnected();
}
_updateStatusDescription();
});
if (needdeivice) {
_handleDeviceConnected();
}
}
/// 处理设备连接成功事件
@ -347,9 +393,8 @@ class PairingController extends GetxController {
/// 启动Companion设备关联流程
Future<void> _startCompanionAssociation() async {
// 如果不是Android平台,已跳转,已完成关联或正在进行关联,直接返回
if (!Platform.isAndroid ||
_isNavigatingToHome ||
// 如果已跳转,已完成关联或正在进行关联,直接返回
if (_isNavigatingToHome ||
_companionAssociated.value ||
_isAssociating ||
!isDeviceConnected) {
@ -367,7 +412,7 @@ class PairingController extends GetxController {
// 检查是否支持Companion功能
final isSupported = await _bleService.isCompanionSupported();
if (!isSupported) {
Logger.info('设备不支持Companion功能,跳过关联');
Logger.i(_tag, '设备不支持Companion功能,跳过关联');
_companionAssociated.value = true;
_autoNavigateToHome();
return;
@ -380,7 +425,7 @@ class PairingController extends GetxController {
return;
}
Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}');
Logger.i(_tag, '开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}');
var success = await _bleService.startDeviceAssociation(
deviceNamePattern: currentDevice!.name,
@ -389,7 +434,7 @@ class PairingController extends GetxController {
_isAssociating = false;
if (success) {
Logger.info('Companion设备关联成功');
Logger.i(_tag, 'Companion设备关联成功');
_companionAssociated.value = true;
_userRejectedAssociation.value = false;
@ -400,14 +445,14 @@ class PairingController extends GetxController {
// 关联成功后跳转
_autoNavigateToHome();
} else {
Logger.error('Companion设备关联失败或被用户拒绝');
Logger.e(_tag, 'Companion设备关联失败或被用户拒绝');
// 关联失败 - 用户拒绝关联
_userRejectedAssociation.value = true;
// 确保UI状态更新
_updateStatusDescription();
}
} catch (e) {
Logger.error('Companion设备关联过程发生错误: ${e.toString()}');
Logger.e(_tag, 'Companion设备关联过程发生错误: ${e.toString()}');
_isAssociating = false;
_userRejectedAssociation.value = true;
_updateStatusDescription();
@ -416,13 +461,13 @@ class PairingController extends GetxController {
/// 自动跳转到主页
Future<void> _autoNavigateToHome() async {
if (needdeivice && (_isNavigatingToHome || !isDeviceConnected)) return;
if (_isNavigatingToHome || !isDeviceConnected) return;
// 启动BLE后台服务
await _startAgentService();
_isNavigatingToHome = true;
Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页');
Logger.i(_tag, '已找到连接的设备 ${currentDevice!.name},自动跳转到主页');
Future.delayed(const Duration(milliseconds: 500), () {
Get.offAllNamed(Routes.home);
@ -432,12 +477,13 @@ class PairingController extends GetxController {
/// 启动BLE后台服务
Future<void> _startAgentService() async {
try {
if (needdeivice && !isDeviceConnected) return;
// 注释掉设备连接检查,确保无论是否连接设备都能启动服务
// if (!isDeviceConnected) return;
final result = await BleManager.to.startAgentService();
Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败');
Logger.i(_tag, result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败');
} catch (e) {
Logger.error('启动BLE后台服务发生错误: ${e.toString()}');
Logger.e(_tag, '启动BLE后台服务发生错误: ${e.toString()}');
}
}
@ -445,9 +491,9 @@ class PairingController extends GetxController {
Future<void> _stopAgentService() async {
try {
final result = await BleManager.to.stopAgentService();
Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败');
Logger.i(_tag, result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败');
} catch (e) {
Logger.error('停止BLE后台服务发生错误: ${e.toString()}');
Logger.e(_tag, '停止BLE后台服务发生错误: ${e.toString()}');
}
}
@ -470,15 +516,82 @@ class PairingController extends GetxController {
} else if (Platform.isAndroid && !isBluetoothOn) {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
} else if (Platform.isIOS) {
openAppSettings();
// iOS上优先尝试打开蓝牙设置
if (!isBluetoothOn) {
// iOS上无法直接打开蓝牙设置,只能打开应用设置
AppSettings.openAppSettings();
} else {
// 如果蓝牙开启但有其他权限问题,打开应用设置
openAppSettings();
}
} else {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
}
} catch (e) {
Logger.error('打开设置失败: ${e.toString()}');
Logger.e(_tag, '打开设置失败: ${e.toString()}');
}
}
/// 重新初始化蓝牙
Future<void> reinitializeBluetooth() async {
try {
isLoading.value = true;
statusDescription.value = '正在重新初始化蓝牙...';
// iOS上尝试重置权限状态
if (Platform.isIOS) {
await Future.delayed(Duration(milliseconds: 500));
// 检查蓝牙权限
PermissionStatus bluetoothStatus = await Permission.bluetooth.status;
Logger.i(_tag, "iOS蓝牙权限当前状态: $bluetoothStatus");
if (bluetoothStatus.isPermanentlyDenied) {
Logger.i(_tag, "iOS蓝牙权限处于永久拒绝状态,提示用户手动开启");
_showPermissionSettingsDialog('蓝牙权限被拒绝,请在设置中手动开启');
return;
}
// 尝试重新请求权限
if (!bluetoothStatus.isGranted) {
bluetoothStatus = await Permission.bluetooth.request();
Logger.i(_tag, "iOS蓝牙权限请求结果: $bluetoothStatus");
}
// 尝试检查蓝牙状态,这会重新触发系统权限弹窗
try {
bool isEnabled = await _bluetoothService.isBluetoothEnabled();
Logger.i(_tag, "iOS蓝牙状态: ${isEnabled ? '开启' : '关闭'}");
} catch (e) {
Logger.e(_tag, "iOS蓝牙状态检查失败: ${e.toString()}");
}
// 检查Companion功能(iOS 18+)
try {
bool isSupported = await _bleService.isCompanionSupported();
if (isSupported) {
Logger.i(_tag, "iOS 18+ Companion功能可用,尝试初始化");
// 尝试预热AccessorySetupKit
await _bleService.getPairedMacAddress();
}
} catch (e) {
Logger.w(_tag, "iOS Companion功能检查失败: ${e.toString()}");
}
}
// 重新检查权限
if (await _checkAndRequestPermissions()) {
// 重新检查蓝牙状态
await _checkBluetoothAndRefresh();
}
} catch (e) {
Logger.e(_tag, '重新初始化蓝牙失败: ${e.toString()}');
} finally {
isLoading.value = false;
_updateStatusDescription();
}
}
/// 打开应用设置
Future<void> openAppSettings() async {
try {
@ -488,7 +601,7 @@ class PairingController extends GetxController {
reinitializeBluetooth();
});
} catch (e) {
Logger.error('打开应用设置失败: ${e.toString()}');
Logger.e(_tag, '打开应用设置失败: ${e.toString()}');
}
}
@ -501,38 +614,13 @@ class PairingController extends GetxController {
/// 跳过连接
void skipConnecting() async {
// 启动BLE后台服务
// 即使没有设备连接,也启动BLE后台服务
await _startAgentService();
_isNavigatingToHome = true;
// 记录跳过配对的日志
Logger.i(_tag, '用户跳过设备配对,直接跳转到主页');
Get.offAllNamed(Routes.home);
}
/// 重新初始化蓝牙
Future<void> reinitializeBluetooth() async {
try {
isLoading.value = true;
statusDescription.value = '正在重新初始化蓝牙...';
// iOS上尝试重置权限状态
if (Platform.isIOS) {
await Future.delayed(Duration(milliseconds: 500));
if (await Permission.bluetooth.isPermanentlyDenied) {
Logger.info("iOS蓝牙权限仍处于永久拒绝状态,尝试直接使用蓝牙功能");
// 在永久拒绝状态下,尝试使用蓝牙功能,iOS可能会再次弹出系统权限请求
_bluetoothService.isBluetoothEnabled();
}
}
// 重新检查权限
if (await _checkAndRequestPermissions()) {
// 重新检查蓝牙状态
await _checkBluetoothAndRefresh();
}
} catch (e) {
Logger.error('重新初始化蓝牙失败: ${e.toString()}');
} finally {
isLoading.value = false;
_updateStatusDescription();
}
}
}

78
lib/modules/settings/controllers/settings_controller.dart

@ -12,6 +12,9 @@ import '../../../data/models/user_Info.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:async';
import 'package:app_settings/app_settings.dart';
class SettingsController extends GetxController {
// 语言设置
@ -101,6 +104,81 @@ class SettingsController extends GetxController {
final appVersion = ''.obs;
final buildNumber = ''.obs;
// 权限相关
// 权限组,用于在UI中显示
final List<Map<String, dynamic>> permissionGroups = [
{
'name': '通讯权限',
'icon': Icons.phone_android,
'permissions': [
{'type': Permission.phone, 'name': '电话', 'description': '用于拨打电话功能'},
{'type': Permission.sms, 'name': '短信', 'description': '用于发送短信功能'},
{'type': Permission.contacts, 'name': '通讯录', 'description': '用于访问联系人'}
]
},
{
'name': '日程权限',
'icon': Icons.calendar_today,
'permissions': [
{'type': Permission.calendarWriteOnly, 'name': '日历写入', 'description': '用于创建日历事件和提醒'},
{'type': Permission.calendarFullAccess, 'name': '日历完全访问', 'description': '用于读取和修改日历事件'}
]
},
{
'name': '存储权限',
'icon': Icons.storage,
'permissions': [
{'type': Permission.storage, 'name': '存储', 'description': '用于保存录音和文件'},
{'type': Permission.photos, 'name': '照片', 'description': '用于选择和保存照片'}
]
},
{
'name': '位置权限',
'icon': Icons.location_on,
'permissions': [
{'type': Permission.location, 'name': '位置', 'description': '用于获取当前位置信息'},
{'type': Permission.locationAlways, 'name': '后台位置', 'description': '用于后台获取位置信息'}
]
},
{
'name': '媒体权限',
'icon': Icons.mic,
'permissions': [
{'type': Permission.microphone, 'name': '麦克风', 'description': '用于语音识别和录音'},
{'type': Permission.bluetooth, 'name': '蓝牙', 'description': '用于连接蓝牙设备'}
]
}
];
// 检查单个权限状态
Future<PermissionStatus> checkPermission(Permission permission) async {
return await permission.status;
}
// 请求单个权限
Future<PermissionStatus> requestPermission(Permission permission) async {
return await permission.request();
}
// 打开应用设置
void openSettings() {
openAppSettings();
}
// 检查所有权限状态并返回结果
Future<Map<Permission, PermissionStatus>> checkAllPermissions() async {
Map<Permission, PermissionStatus> statuses = {};
for (var group in permissionGroups) {
for (var perm in group['permissions']) {
Permission permission = perm['type'];
statuses[permission] = await permission.status;
}
}
return statuses;
}
@override
void onInit() {
super.onInit();

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

@ -0,0 +1,324 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import '../controllers/settings_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui';
class PermissionsView extends GetView<SettingsController> {
const PermissionsView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
),
onPressed: () => Get.back(),
),
title: Text(
'权限管理'.tr,
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
),
body: FutureBuilder<Map<Permission, PermissionStatus>>(
future: controller.checkAllPermissions(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('获取权限状态失败: ${snapshot.error}'.tr));
}
final permissionStatuses = snapshot.data ?? {};
return ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: [
// 通讯权限组
_buildPermissionSection('通讯权限', Icons.phone_android, [
Permission.phone,
Permission.sms
], permissionStatuses, isDarkMode),
SizedBox(height: 20.h),
// 日历权限组
_buildPermissionSection('日历权限', Icons.calendar_today, [
Permission.calendarFullAccess
], permissionStatuses, isDarkMode),
SizedBox(height: 20.h),
// 位置权限组
_buildPermissionSection('位置权限', Icons.location_on, [
Permission.location
], permissionStatuses, isDarkMode),
],
);
},
),
);
}
// 构建权限组区域
Widget _buildPermissionSection(String title, IconData icon, List<Permission> permissions,
Map<Permission, PermissionStatus> statuses, bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 组标题
Padding(
padding: EdgeInsets.only(left: 4.w, bottom: 8.h),
child: Text(
title.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: Column(
children: _buildPermissionItems(permissions, statuses, isDarkMode),
),
),
),
),
],
);
}
// 构建权限项列表
List<Widget> _buildPermissionItems(List<Permission> permissions,
Map<Permission, PermissionStatus> statuses, bool isDarkMode) {
final List<Widget> items = [];
// 获取描述文本
String getDescription(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 '应用功能需要';
}
}
// 获取名称
String getName(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();
}
}
for (int i = 0; i < permissions.length; i++) {
final permission = permissions[i];
final status = statuses[permission] ?? PermissionStatus.denied;
items.add(
_buildPermissionItem(
name: getName(permission),
description: getDescription(permission),
status: status,
permission: permission,
isDarkMode: isDarkMode,
),
);
// 添加分隔线,最后一个不加
if (i < permissions.length - 1) {
items.add(Divider(
height: 1,
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200],
));
}
}
return items;
}
// 构建单个权限项
Widget _buildPermissionItem({
required String name,
required String description,
required PermissionStatus status,
required Permission permission,
required bool isDarkMode,
}) {
return Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name.tr,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 4.h),
Text(
description.tr,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600],
),
),
SizedBox(height: 4.h),
_buildStatusText(status, isDarkMode),
],
),
),
Switch(
value: status.isGranted,
onChanged: (value) {
if (value) {
_handlePermissionAction(permission, status);
} else if (status.isGranted) {
controller.openSettings();
}
},
activeColor: Colors.blue[600],
),
],
),
);
}
// 状态文本
Widget _buildStatusText(PermissionStatus status, bool isDarkMode) {
String text;
Color color;
if (status.isGranted) {
text = '已授权'.tr;
color = Colors.green;
} else if (status.isDenied) {
text = '未授权'.tr;
color = Colors.red;
} else if (status.isPermanentlyDenied) {
text = '需要在系统设置中开启'.tr;
color = Colors.grey;
} else if (status.isLimited) {
text = '部分授权'.tr;
color = Colors.orange;
} else {
text = '未知状态'.tr;
color = Colors.grey;
}
return Text(
text,
style: TextStyle(
color: color,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
);
}
// 处理权限请求
Future<void> _handlePermissionAction(Permission permission, PermissionStatus status) async {
if (status.isPermanentlyDenied) {
// 打开应用设置
controller.openSettings();
return;
}
if (!status.isGranted) {
// 请求权限
final result = await controller.requestPermission(permission);
if (result.isGranted) {
Get.snackbar('成功'.tr, '权限已授予'.tr, snackPosition: SnackPosition.BOTTOM);
} else if (result.isPermanentlyDenied) {
Get.dialog(
AlertDialog(
title: Text('需要权限'.tr),
content: Text('此权限需要在设置中手动开启'.tr),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('取消'.tr),
),
TextButton(
onPressed: () {
Get.back();
controller.openSettings();
},
child: Text('去设置'.tr),
),
],
),
);
}
// 刷新界面
Get.forceAppUpdate();
}
}
}

92
lib/modules/settings/views/settings_view.dart

@ -355,7 +355,7 @@ class SettingsView extends GetView<SettingsController> {
Padding(
padding: EdgeInsets.only(left: 4.w, bottom: 8.h),
child: Text(
'systemSetting'.tr, // 系统设置
'systemSettings'.tr, // 系统设置
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
@ -586,6 +586,22 @@ class SettingsView extends GetView<SettingsController> {
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
// 添加权限管理设置项
_buildSettingItem(
icon: Icons.security,
iconColor: Colors.orange,
title: '权限管理'.tr,
isDarkMode: isDarkMode,
onTap: () {
Get.toNamed(Routes.permissions);
},
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 退出登录
// _buildSimpleNavigationSetting(
// title: 'logout'.tr, // 退出登录
@ -1518,4 +1534,78 @@ class SettingsView extends GetView<SettingsController> {
),
);
}
// 添加权限管理设置项
Widget _buildSettingItem({
required IconData icon,
required Color iconColor,
required String title,
required bool isDarkMode,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.05)
: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
icon,
size: 16.w,
color: iconColor,
),
),
SizedBox(width: 12.w),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.05)
: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
Icons.chevron_right,
size: 16.w,
color: isDarkMode ? Colors.white70 : Colors.grey[500],
),
),
],
),
),
);
}
}

8
lib/routes/app_pages.dart

@ -28,6 +28,7 @@ import '../modules/pairing/views/pairing_view.dart';
import '../modules/pairing/bindings/pairing_binding.dart';
import './app_routes.dart';
import '../modules/settings/views/settings_view.dart';
import '../modules/settings/views/permissions_view.dart';
import '../modules/settings/bindings/settings_binding.dart';
import '../modules/opus_test/views/opus_test_view.dart';
import '../modules/opus_test/bindings/opus_test_binding.dart';
@ -181,5 +182,12 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.permissions,
page: () => const PermissionsView(),
binding: SettingsBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
];
}

1
lib/routes/app_routes.dart

@ -16,6 +16,7 @@ abstract class Routes {
static const OTA_UPGRADE = '/ota_upgrade'; // OTA升级
static const timbre = '/timbre'; // 音色设置
static const settings = '/settings'; // 系统设置
static const permissions = '/permissions'; // 权限管理
static const recording = '/recording'; // 录音功能
// 会议相关

19
local_plugins/ble_service/LICENSE

@ -0,0 +1,19 @@
Copyright (c) 2024 Deep Voice
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

15
local_plugins/ble_service/ios/Assets/ble_service-Info.plist

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限与设备进行通信和传输音频数据</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限与设备进行通信和传输音频数据</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>audio</string>
</array>
</dict>
</plist>

195
local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift

@ -0,0 +1,195 @@
import Foundation
import CoreBluetooth
import os.log
// 条件编译只导入在iOS 18+上可用的框架
#if canImport(AccessorySetupKit) && swift(>=5.9)
import AccessorySetupKit
import UIKit
#endif
class BleAccessorySetup: NSObject {
// 单例实现
static let shared = BleAccessorySetup()
// 基本属性
private var bleService: Any?
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup")
// iOS 18+ 特定属性和回调
#if canImport(AccessorySetupKit) && swift(>=5.9)
@available(iOS 18.0, *)
class AccessorySessionWrapper {
var session: ASAccessorySession
init() {
self.session = ASAccessorySession()
}
}
var accessorySessionWrapper: Any?
typealias DeviceDiscoveredCallback = (UUID) -> Void
private var onDeviceDiscovered: DeviceDiscoveredCallback?
#endif
private override init() {
super.init()
}
// 设置BLE服务实例
func setBleService(_ service: Any) {
self.bleService = service
}
// 设置设备发现回调
func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
self.onDeviceDiscovered = callback
}
#endif
}
// 初始化设备发现会话
func initializeDiscoverySession() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("初始化iOS 18 AccessorySetupKit会话", log: logger, type: .info)
// 创建会话
accessorySessionWrapper = AccessorySessionWrapper()
// 激活会话并设置事件处理
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper {
wrapper.session.activate(on: .main) { [weak self] event in
guard let self = self else { return }
switch event.eventType {
case .activated:
os_log("AccessorySetupKit会话已激活", log: self.logger, type: .info)
case .accessoryAdded:
if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier {
os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName)
self.onDeviceDiscovered?(uuid)
}
case .accessoryRemoved:
if let accessory = event.accessory {
os_log("设备已移除: %{public}@", log: self.logger, type: .info, accessory.displayName)
}
// 根据WWDC24视频,正确的事件类型为accessoryChanged
case .accessoryChanged:
if let accessory = event.accessory {
os_log("设备已更新: %{public}@", log: self.logger, type: .info, accessory.displayName)
}
// 根据WWDC24视频,失败事件类型已变更
case .pickerSetupFailed:
if let error = event.error {
os_log("会话出错: %{public}@", log: self.logger, type: .error, error.localizedDescription)
}
default:
os_log("收到其他会话事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue)
}
}
// 检查现有授权设备
if !wrapper.session.accessories.isEmpty {
os_log("发现%{public}d个已授权设备", log: logger, type: .info, wrapper.session.accessories.count)
// 如果有现有设备,触发回调
for accessory in wrapper.session.accessories {
if let uuid = accessory.bluetoothIdentifier {
os_log("处理已授权设备: %{public}@", log: logger, type: .info, accessory.displayName)
self.onDeviceDiscovered?(uuid)
}
}
}
}
} else {
os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info)
}
#else
os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info)
#endif
}
// 显示设备选择器
func showDevicePicker() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper {
os_log("显示iOS 18设备选择器", log: logger, type: .info)
// 创建描述符和选择器项,这些API也需要iOS 18
@available(iOS 18.0, *)
func createAndShowPicker() {
// 根据WWDC24视频,正确的类名是ASDiscoveryDescriptor
let descriptor = ASDiscoveryDescriptor()
// 设置过滤条件
// 使用ASBluetoothCompanyIdentifier类型
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(UInt16(0xABEF)) // BleConst.MANUFACTURER_ID
descriptor.bluetoothServiceUUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") // BleConst.PRIMARY_SERVICE_UUID
// 创建默认图像以防系统图标不可用
let headphonesImage = UIImage(systemName: "headphones") ?? UIImage()
// 根据WWDC24视频,正确的类名是ASPickerDisplayItem
let displayItem = ASPickerDisplayItem(
name: "语音助理耳机",
productImage: headphonesImage,
descriptor: descriptor
)
// 显示选择器
wrapper.session.showPicker(for: [displayItem]) { error in
if let error = error {
// 修复OSLog的错误用法
os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription)
}
}
}
// 调用内部函数
createAndShowPicker()
} else {
os_log("会话未初始化", log: logger, type: .error)
}
} else {
os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info)
}
#else
os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info)
#endif
}
// 处理发现的设备
func handleDiscoveredAccessory(uuid: UUID) {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
guard let service = bleService as? BleService else {
os_log("BLE服务未设置", log: logger, type: .error)
return
}
// 获取中央管理器
let centralManager = service.getCentralManager()
// 通过UUID检索外设
let peripherals = centralManager.retrievePeripherals(withIdentifiers: [uuid])
if let peripheral = peripherals.first {
os_log("连接到UUID对应的设备: %{public}@", log: logger, type: .info, peripheral.name ?? uuid.uuidString)
service.connect(to: peripheral)
} else {
os_log("未找到UUID对应的设备: %{public}@", log: logger, type: .error, uuid.uuidString)
}
}
#endif
}
}

75
local_plugins/ble_service/ios/Classes/BleConst.swift

@ -1,28 +1,30 @@
import Foundation
import CoreBluetooth
/// BLE服务常量
struct BleConst {
/**
* BLE服务常量
*/
class BleConst {
// ================== 协议相关UUID ==================
/// 主服务UUID - 0000ABC0-0000-1111-2222-123456789ABC
/** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */
static let PRIMARY_SERVICE_UUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc")
/// 音频服务UUID - 00001801-0000-1000-8000-00805f9b34fb
/** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */
static let AUDIO_SERVICE_UUID = CBUUID(string: "0000ae00-0000-1000-8000-00805f9b34fb")
/// 接收音频特征UUID - 0000ABC1-0000-1111-2222-123456789ABC
/** 接收音频特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */
static let RECEIVE_AUDIO_CHAR_UUID = CBUUID(string: "0000ae02-0000-1000-8000-00805f9b34fb")
/// 写入特征UUID - 0000ABC1-0000-1111-2222-123456789ABC
/** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */
static let WRITE_CHAR_UUID = CBUUID(string: "0000abc1-0000-1111-2222-123456789abc")
/// 通知特征UUID - 0000ABC2-0000-1111-2222-123456789ABC
/** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */
static let NOTIFY_CHAR_UUID = CBUUID(string: "0000abc2-0000-1111-2222-123456789abc")
/// 客户端特征配置描述符UUID
static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "0000fd7b-0000-1000-8000-00805f9b34fb")
/** 客户端特征配置描述符UUID */
static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb")
/// BLE扫描Action
/** BLE扫描Action */
static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN"
// ================== 连接状态 ==================
@ -33,58 +35,59 @@ struct BleConst {
static let STATE_ERROR = 4
// ================== 广播厂商信息 ==================
/// 厂商ID - 0xABEF
/** 厂商ID - 文档中定义为0xABEF */
static let MANUFACTURER_ID: UInt16 = 0xABEF
/// 客户ID - 0xA001
/** 客户ID - 文档中定义为0xA001 */
static let CUSTOMER_ID: UInt16 = 0xA001
/// 产品ID - 0xA001
/** 产品ID - 文档中定义为0xA001 */
static let PRODUCT_ID: UInt16 = 0xA001
/// 蓝牙状态-未连接
/** 蓝牙状态-未连接 */
static let BT_STATE_DISCONNECTED: UInt8 = 0x00
/// 蓝牙状态-已连接
/** 蓝牙状态-已连接 */
static let BT_STATE_CONNECTED: UInt8 = 0x0C
// ================== 协议格式 ==================
/// APP请求帧头
/** APP请求帧头 */
static let APP_REQUEST_HEADER: UInt8 = 0xAA
/// 设备响应帧头
/** 设备响应帧头 */
static let DEVICE_RESPONSE_HEADER: UInt8 = 0xBB
/// 设备主动上报帧头
/** 设备主动上报帧头 */
static let DEVICE_NOTIFY_HEADER: UInt8 = 0xCC
// ================== 协议指令 (CMD) ==================
/// 获取耳机版本号
/** 获取耳机版本号 */
static let CMD_GET_VERSION: UInt8 = 0x01
/// 获取耳机产品ID
/** 获取耳机产品ID */
static let CMD_GET_PRODUCT_ID: UInt8 = 0x02
/// 获取耳机颜色ID
/** 获取耳机颜色ID */
static let CMD_GET_COLOR_ID: UInt8 = 0x03
/// 获取耳机电量ID
/** 获取耳机电量ID */
static let CMD_GET_BATTERY_INFO: UInt8 = 0x04
/// APP控制编解码指令
/** APP控制编解码指令 */
static let CMD_CONTROL_CODEC: UInt8 = 0x05
/// 音量增大
/** 音量增大 */
static let CMD_VOLUME_UP: UInt8 = 0x06
/// 音量减小
/** 音量减小 */
static let CMD_VOLUME_DOWN: UInt8 = 0x07
/// A2DP播放
/** A2DP播放 */
static let CMD_PLAY_A2DP: UInt8 = 0x08
/// 唤醒指令
/** 唤醒指令 */
static let CMD_WAKE_UP: UInt8 = 0x11
// ================== 编解码控制值 ==================
/// 关闭编解码指令
/** 关闭编解码指令 */
static let CODEC_CONTROL_CLOSE: UInt8 = 0x00
/// 打开解码指令
/** 音乐或者通话远端声音 */
static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1
/// 通过A2DP播放
/** mic和dac(音乐或者通话远端)声音 */
static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2
/// 打开编码指令
/** 打开编码指令 */
static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1
/// 左声道
/** 左声道 */
static let AUDIO_CHANNEL_LEFT: UInt8 = 0x01
/// 右声道
/** 右声道 */
static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02
/// 立体声
/** 立体声 */
static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03
}

25
local_plugins/ble_service/ios/Classes/BleDelegate.swift

@ -1,25 +0,0 @@
import Foundation
/// BLE服务回调代理协议
protocol BleDelegate: AnyObject {
/// 扫描结果回调
/// - Parameter devices: 设备列表,每个设备包含id、name、rssi和uuids等信息
func onScanResult(devices: [[String: Any]])
/// 连接状态改变回调
/// - Parameter state: 连接状态,参考BleConst中的STATE_*常量
func onConnectionStateChanged(state: Int)
/// 音频数据接收回调
/// - Parameter data: 接收到的音频数据
func onAudioDataReceived(data: Data)
/// 唤醒信号接收回调
func onWakeupSignalReceived()
/// 设备信息接收回调
/// - Parameters:
/// - infoType: 信息类型
/// - infoData: 信息数据
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any])
}

1067
local_plugins/ble_service/ios/Classes/BleService.swift

File diff suppressed because it is too large

2
local_plugins/ble_service/ios/Classes/BleServicePlugin.h

@ -1,4 +1,4 @@
#import <Flutter/Flutter.h>
@interface BleServicePlugin : NSObject<FlutterPlugin>
@end

360
local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift

@ -1,140 +1,257 @@
import Flutter
import UIKit
import CoreBluetooth
import os
public class SwiftBleServicePlugin: NSObject, FlutterPlugin, BleDelegate {
private let TAG = "BleServicePlugin"
// 方法通道
private var methodChannel: FlutterMethodChannel?
// 事件通道
@available(iOS 13.0, *)
public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
// MARK: - 属性
private var methodChannel: FlutterMethodChannel
private var dataEventSink: FlutterEventSink?
private var statusEventSink: FlutterEventSink?
// BLE服务实例
private let bleService = BleService.shared
public static func register(with registrar: FlutterPluginRegistrar) {
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger())
// 创建插件实例
let instance = SwiftBleServicePlugin()
instance.methodChannel = methodChannel
// 设置方法通道处理器
registrar.addMethodCallDelegate(instance, channel: methodChannel)
// 设置事件通道
instance.setupEventChannels(with: registrar)
// MARK: - 初始化方法
init(_ channel: FlutterMethodChannel) {
self.methodChannel = channel
super.init()
// 初始化BLE服务
_ = instance.bleService.initialize()
// 设置BLE服务回调
BleService.shared.setDelegate(self)
// 添加自身作为BLE回调
_ = instance.bleService.addDelegate(instance)
// 初始化AccessorySetup
BleAccessorySetup.shared.setBleService(BleService.shared)
BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in
self?.handleDiscoveredDevice(uuid: uuid)
}
BleAccessorySetup.shared.initializeDiscoverySession()
}
/// 设置事件通道
private func setupEventChannels(with registrar: FlutterPluginRegistrar) {
// 数据接收事件通道
let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger())
dataEventChannel.setStreamHandler(StreamHandler { [weak self] sink in
self?.dataEventSink = sink
} onCancel: { [weak self] in
self?.dataEventSink = nil
})
// MARK: - FlutterPlugin Protocol
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger())
let instance = SwiftBleServicePlugin(channel)
registrar.addMethodCallDelegate(instance, channel: channel)
// 状态更新事件通道
// 注册事件通道
let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger())
let statusEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/status_events", binaryMessenger: registrar.messenger())
statusEventChannel.setStreamHandler(StreamHandler { [weak self] sink in
self?.statusEventSink = sink
} onCancel: { [weak self] in
self?.statusEventSink = nil
})
}
/// 发送事件到Flutter
private func sendEvent(_ eventSink: FlutterEventSink?, _ data: Any) {
DispatchQueue.main.async {
eventSink?(data)
}
dataEventChannel.setStreamHandler(instance)
statusEventChannel.setStreamHandler(instance)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
NSLog("Method call: \(call.method)")
switch call.method {
case "isConnected":
result(bleService.isConnected())
result(BleService.shared.isConnected())
case "getBatteryInfo":
result(bleService.getBatteryInfo())
case "getBatteryLevel":
// 获取电池信息,通过回调返回结果
if BleService.shared.getBatteryInfo() {
result(0) // 通过回调返回实际值
} else {
result(FlutterError(code: "UNAVAILABLE",
message: "无法获取电池信息",
details: nil))
}
case "isCompanionSupported":
// 检查设备是否支持iOS 18的AccessorySetupKit功能
#if canImport(AccessorySetupKit)
if #available(iOS 18.0, *) {
// 设备运行iOS 18+,支持AccessorySetupKit
result(true)
} else {
// 设备运行iOS 17或更低版本
result(false)
}
#else
// 编译时没有AccessorySetupKit框架
result(false)
#endif
case "getPairedMacAddress":
// 获取已配对设备的标识符
#if canImport(AccessorySetupKit)
if #available(iOS 18.0, *) {
if let wrapper = BleAccessorySetup.shared.accessorySessionWrapper as? BleAccessorySetup.AccessorySessionWrapper {
// 检查是否有配对设备
if let firstAccessory = wrapper.session.accessories.first,
let btId = firstAccessory.bluetoothIdentifier {
// 在iOS中,我们返回UUID字符串作为"MAC地址"的等价物
result(btId.uuidString)
} else {
// 无配对设备
result(nil)
}
} else {
// 会话未初始化
result(nil)
}
} else {
// iOS版本太低
result(nil)
}
#else
// 编译环境不支持
result(nil)
#endif
case "registerBackgroundScan":
// iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注册
#if canImport(AccessorySetupKit)
if #available(iOS 18.0, *) {
// 在iOS 18+上,不需要额外操作
result(true)
} else {
// iOS版本太低
result(false)
}
#else
result(false)
#endif
case "unregisterBackgroundScan":
// iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注销
result(true)
case "clearAssociations":
// 清除设备关联
#if canImport(AccessorySetupKit)
if #available(iOS 18.0, *) {
if let wrapper = BleAccessorySetup.shared.accessorySessionWrapper as? BleAccessorySetup.AccessorySessionWrapper {
// 遍历所有配对设备
for accessory in wrapper.session.accessories {
if let btId = accessory.bluetoothIdentifier {
// 移除设备的系统处理已经集成在设置中,这里什么都不做
os_log("设备需要在系统设置中手动移除: %{public}@",
log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"),
type: .info,
btId.uuidString)
}
}
}
// 返回成功
result(true)
} else {
// iOS版本太低
result(false)
}
#else
result(false)
#endif
case "getDeviceVersion":
result(bleService.getDeviceVersion())
result(BleService.shared.getDeviceVersion())
case "getProductId":
result(bleService.getProductId())
result(BleService.shared.getProductId())
case "getBatteryInfo":
result(BleService.shared.getBatteryInfo())
case "openEncoder":
result(bleService.openEncoder())
result(BleService.shared.openEncoder())
case "openDecoder":
result(bleService.openDecoder())
result(BleService.shared.openDecoder())
case "openA2DPDecoder":
result(bleService.openA2DPDecoder())
result(BleService.shared.openA2DPDecoder())
case "closeCodec":
result(bleService.closeCodec())
result(BleService.shared.closeCodec())
case "playA2DP":
result(bleService.playA2DP())
case "registerBackgroundScan":
result(bleService.registerBackgroundScan())
case "unregisterBackgroundScan":
result(bleService.unregisterBackgroundScan())
result(BleService.shared.playA2DP())
case "startBleService":
guard let config = call.arguments as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "配置参数无效", details: nil))
return
// iOS 18+ AccessorySetupKit相关方法
case "startDeviceAssociation":
#if canImport(AccessorySetupKit)
if #available(iOS 18.0, *) {
BleAccessorySetup.shared.showDevicePicker()
result(true)
} else {
result(FlutterError(code: "UNAVAILABLE",
message: "AccessorySetupKit需要iOS 18或更高版本",
details: nil))
}
// 处理配置参数
if let address = config["mac"] as? String {
// 如果提供了设备地址,尝试连接
bleService.connect(address: address)
}
// 开始扫描,寻找设备
let scanSuccess = bleService.startScan()
result(scanSuccess)
case "stopBleService":
// 停止扫描和断开连接
_ = bleService.stopScan()
if bleService.isConnected() {
bleService.disconnect()
}
result(true)
#else
result(FlutterError(code: "UNAVAILABLE",
message: "编译环境不支持AccessorySetupKit",
details: nil))
#endif
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - BleDelegate Methods
// MARK: - 私有方法
private func handleDiscoveredDevice(uuid: UUID) {
BleAccessorySetup.shared.handleDiscoveredAccessory(uuid: uuid)
}
func onScanResult(devices: [[String: Any]]) {
// 目前Flutter层没有直接使用此事件,可以选择不转发
private func sendEvent<T>(_ eventSink: FlutterEventSink?, data: T, errorPrefix: String) {
if let eventSink = eventSink {
DispatchQueue.main.async {
eventSink(data)
}
}
}
}
// MARK: - FlutterStreamHandler
@available(iOS 13.0, *)
extension SwiftBleServicePlugin: FlutterStreamHandler {
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
if let args = arguments as? [String: Any], let type = args["type"] as? String {
if type == "data" {
dataEventSink = events
} else if type == "status" {
statusEventSink = events
}
} else {
// 默认行为:根据注册的通道来确定事件类型
if arguments == nil {
// 如果无参数,根据函数被调用的顺序确定
if dataEventSink == nil {
dataEventSink = events
} else {
statusEventSink = events
}
}
}
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
if let args = arguments as? [String: Any], let type = args["type"] as? String {
if type == "data" {
dataEventSink = nil
} else if type == "status" {
statusEventSink = nil
}
} else {
// 同时清除两个sink
dataEventSink = nil
statusEventSink = nil
}
return nil
}
}
// MARK: - BleService.Callback
@available(iOS 13.0, *)
extension SwiftBleServicePlugin: BleService.Callback {
func onScanResult(devices: [[String : Any]]) {
sendEvent(statusEventSink,
data: ["type": "scanResult", "devices": devices],
errorPrefix: "发送扫描结果异常")
}
func onConnectionStateChanged(state: Int) {
// 将连接状态变化通过状态通道发送
let stateStr: String
var stateStr = "Unknown"
switch state {
case BleConst.STATE_DISCONNECTED:
stateStr = "Disconnected"
@ -156,54 +273,29 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin, BleDelegate {
"stateStr": stateStr
]
sendEvent(statusEventSink, stateMap)
sendEvent(statusEventSink, data: stateMap, errorPrefix: "发送连接状态异常")
}
func onAudioDataReceived(data: Data) {
// 将音频数据通过数据通道发送
let dataMap: [String: Any] = [
"type": "audioData",
"data": FlutterStandardTypedData(bytes: data)
"data": data
]
sendEvent(dataEventSink, dataMap)
}
func onWakeupSignalReceived() {
// 将唤醒事件通过状态通道发送
sendEvent(statusEventSink, ["type": "wakeup"])
sendEvent(dataEventSink, data: dataMap, errorPrefix: "发送音频数据异常")
}
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) {
// 将设备信息通过状态通道发送
var resultMap = infoData
resultMap["type"] = "deviceInfo"
resultMap["result"] = infoType
func onDeviceInfoReceived(infoType: Int, infoData: [String : Any]) {
var dataMap: [String: Any] = [
"type": "deviceInfo",
"infoType": infoType
]
sendEvent(statusEventSink, resultMap)
}
}
// MARK: - 事件处理器类
/// 简化的事件处理器类
class StreamHandler: NSObject, FlutterStreamHandler {
private let onListen: (FlutterEventSink) -> Void
private let onCancel: () -> Void
init(onListen: @escaping (FlutterEventSink) -> Void, onCancel: @escaping () -> Void) {
self.onListen = onListen
self.onCancel = onCancel
super.init()
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
onListen(events)
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
onCancel()
return nil
// 合并infoData
for (key, value) in infoData {
dataMap[key] = value
}
sendEvent(statusEventSink, data: dataMap, errorPrefix: "发送设备信息异常")
}
}

87
local_plugins/ble_service/ios/README.md

@ -0,0 +1,87 @@
# ble_service iOS 集成说明
## 概述
该插件提供了蓝牙低功耗(BLE)设备的连接和通信功能,特别支持BLE耳机设备的音频数据传输。
- 支持 iOS 13.0+ 的基础蓝牙连接功能
- 支持 iOS 18.0+ 的 AccessorySetupKit 集成,实现应用后台唤醒
## 集成步骤
### 1. 权限配置
在应用的 `Info.plist` 中添加以下权限描述:
```xml
<!-- 蓝牙权限 -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要使用蓝牙连接您的耳机设备</string>
<!-- 蓝牙外设权限 -->
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要使用蓝牙连接您的耳机设备</string>
<!-- 后台蓝牙模式 -->
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array>
```
### 2. iOS 18+ AccessorySetupKit 配置
如果需要支持 iOS 18+ 的 AccessorySetupKit 功能(应用后台唤醒),还需要在 `Info.plist` 中添加:
```xml
<!-- 配置 AccessorySetupKit -->
<key>NSAccessorySetupBluetoothCompanyIdentifiers</key>
<array>
<string>ABEF</string> <!-- 耳机厂商ID 0xABEF -->
</array>
<key>NSAccessorySetupBluetoothServices</key>
<array>
<string>0000ABC0-0000-1111-2222-123456789ABC</string> <!-- 主服务 UUID -->
<string>0000AE00-0000-1000-8000-00805F9B34FB</string> <!-- 音频服务 UUID -->
</array>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
```
### 3. 集成到 Runner 项目
1. 确保项目的最低部署版本为 iOS 13.0
2. 在 Xcode 中编辑 `Runner` 项目的 `info.plist`,添加上述配置
3. 如需自定义 UUID 或厂商 ID,请同时修改 `BleConst.swift` 文件中的对应常量
## 使用说明
### 基本流程
1. 首次使用时,应用需要在前台扫描并连接设备
2. 配对成功后,后续可通过 AccessorySetupKit (iOS 18+) 自动唤醒应用
### 支持的命令
- 获取设备版本: `getDeviceVersion()`
- 获取电池信息: `getBatteryInfo()`
- 打开/关闭编解码: `openEncoder()`, `openDecoder()`, `closeCodec()`
- A2DP 控制: `playA2DP()`
## 后台运行说明
1. 该插件支持在后台接收 BLE 通知和数据
2. iOS 18+ 设备上,支持应用在后台/被杀死状态下被设备唤醒
3. 后台保活和状态恢复由系统管理,无需额外代码
## 注意事项
- 使用前确保已在用户设备上获得蓝牙权限
- 由于苹果限制,手动杀死应用后,仍需用户主动重新打开一次应用才能完全激活后台唤醒
- 厂商 ID 和服务 UUID 需要与实际设备匹配

10
local_plugins/ble_service/ios/ble_service.podspec

@ -1,21 +1,23 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint ble_service.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'ble_service'
s.version = '0.0.1'
s.summary = 'BLE服务Flutter插件'
s.summary = 'BLE服务插件,提供蓝牙扫描、连接和通信功能'
s.description = <<-DESC
BLE服务Flutter插件,提供蓝牙设备扫描、连接和通信功能。
BLE服务插件,提供蓝牙扫描、连接和通信功能,支持iOS 18+ AccessorySetupKit唤醒。
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.info_plist = {
'NSAccessorySetupKitSupports' => ['Bluetooth']
}
s.dependency 'Flutter'
s.platform = :ios, '11.0'
s.platform = :ios, '18.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }

8
local_plugins/ble_service/lib/ble_service.dart

@ -161,13 +161,7 @@ class BleService {
}
/// 启动BLE后台服务
/// [config] 配置参数,可包含:
/// - scan_interval: 扫描间隔时间(毫秒)
/// - scan_duration: 扫描持续时间(毫秒)
/// - notification_title: 通知标题
/// - notification_content: 通知内容
/// - mac: 设备地址
/// - device_name: 设备名称
Future<bool> startBleService(Map<String, dynamic> config) async {
try {
final result =

4
local_plugins/ble_service/pubspec.yaml

@ -21,4 +21,6 @@ flutter:
platforms:
android:
package: com.yunqiinnovation.ble_service
pluginClass: BleServicePlugin
pluginClass: BleServicePlugin
ios:
pluginClass: BleServicePlugin

304
local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift

@ -1,6 +1,6 @@
import Foundation
import ExternalAccessory
import CoreBluetooth
import AVFoundation
/// 蓝牙事件类型
enum BluetoothEventType {
@ -14,113 +14,205 @@ enum BluetoothState: String {
case unknown = "unknown"
case off = "off"
case on = "on"
case turningOn = "turningOn"
case turningOff = "turningOff"
case unauthorized = "unauthorized"
case unsupported = "unsupported"
case resetting = "resetting"
}
/// 经典蓝牙助手类,负责iOS上的蓝牙设备管理
/// 简化的蓝牙助手类,专注于iOS上的蓝牙耳机检测
class ClassicBluetoothHelper: NSObject {
private let TAG = "ClassicBluetoothHelper"
// 状态监控相关
private var isMonitoring = false
private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)?
// 用于监控蓝牙系统状态
// 用于监控蓝牙系统状态,延迟初始化
private var centralManager: CBCentralManager?
// 通知观察者
private var accessoryConnectionObserver: NSObjectProtocol?
private var accessoryDisconnectionObserver: NSObjectProtocol?
// 音频路由变化观察者
private var routeChangeObserver: NSObjectProtocol?
override init() {
super.init()
centralManager = CBCentralManager(delegate: nil, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: false])
// 初始化时不立即创建centralManager,延迟到实际需要时
}
deinit {
stopBluetoothStateMonitoring()
}
/// 检查蓝牙是否启用
/// 延迟初始化CBCentralManager,只在实际需要时创建
private func initializeCentralManagerIfNeeded() {
if centralManager == nil {
centralManager = CBCentralManager(
delegate: self,
queue: DispatchQueue.main,
options: [CBCentralManagerOptionShowPowerAlertKey: true]
)
}
}
/// 检查蓝牙是否启用,此方法会触发权限请求
func isBluetoothEnabled() -> Bool {
guard let centralManager = centralManager else { return false }
return centralManager.state == .poweredOn
// 此方法调用会触发权限申请
initializeCentralManagerIfNeeded()
guard let central = centralManager else {
print("检查蓝牙状态失败:centralManager未初始化")
return false
}
// 检查蓝牙状态并记录
let stateDescription: String
let isEnabled: Bool
switch central.state {
case .poweredOn:
stateDescription = "已开启"
isEnabled = true
case .poweredOff:
stateDescription = "已关闭"
isEnabled = false
case .unauthorized:
stateDescription = "未授权"
isEnabled = false
case .unsupported:
stateDescription = "不支持"
isEnabled = false
case .resetting:
stateDescription = "重置中"
isEnabled = false
case .unknown:
stateDescription = "未知"
isEnabled = false
@unknown default:
stateDescription = "未定义状态"
isEnabled = false
}
print("蓝牙状态检查结果: \(stateDescription), isEnabled: \(isEnabled)")
return isEnabled
}
/// 获取已连接的蓝牙设备
/// - Parameter completion: 完成回调
/// 获取已连接的蓝牙耳机,此方法会触发权限请求
func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) {
// 获取所有已连接的外部配件
let accessoryManager = EAAccessoryManager.shared()
let connectedAccessories = accessoryManager.connectedAccessories
// 将配件转换为设备信息数组
let devices = connectedAccessories.map { accessory -> [String: String] in
return [
"name": accessory.name,
"address": accessory.serialNumber // iOS使用序列号作为唯一标识
// 确保蓝牙已初始化
initializeCentralManagerIfNeeded()
// 获取音频设备(主要是蓝牙耳机)
let audioDevices = getConnectedAudioDevices()
// 日志输出
if !audioDevices.isEmpty {
print("已找到\(audioDevices.count)个蓝牙音频设备:")
for device in audioDevices {
print("- \(device["name"] ?? "未知") (\(device["address"] ?? "未知地址"))")
}
} else {
print("未找到蓝牙音频设备")
}
completion(audioDevices, nil)
}
// 获取连接的蓝牙音频设备
private func getConnectedAudioDevices() -> [[String: String]] {
var result: [[String: String]] = []
// 获取当前音频路由
let outputs = AVAudioSession.sharedInstance().currentRoute.outputs
// 过滤蓝牙相关设备
for output in outputs where isBluetoothOutput(output.portType) {
let device: [String: String] = [
"name": output.portName,
"address": cleanDeviceAddress(output.uid)
]
result.append(device)
}
// 返回结果
completion(devices, nil)
return result
}
// 检查是否为蓝牙输出
private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool {
return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE
}
// 清理设备地址
private func cleanDeviceAddress(_ address: String) -> String {
// 移除类似 "-tacl" 的后缀
if let range = address.range(of: "-", options: .backwards) {
return String(address[..<range.lowerBound])
}
return address
}
/// 开始监听蓝牙状态变化
/// - Parameter callback: 状态变化回调
func startBluetoothStateMonitoring(callback: @escaping (BluetoothEventType, [String: Any]) -> Void) {
self.stateCallback = callback
if !isMonitoring {
isMonitoring = true
// 设置蓝牙中央管理器代理
centralManager?.delegate = self
// 初始化蓝牙管理器
initializeCentralManagerIfNeeded()
// 注册蓝牙配件连接/断开通知
// 观察音频路由变化
let notificationCenter = NotificationCenter.default
// 观察配件连接
accessoryConnectionObserver = notificationCenter.addObserver(
forName: NSNotification.Name.EAAccessoryDidConnect,
routeChangeObserver = notificationCenter.addObserver(
forName: AVAudioSession.routeChangeNotification,
object: nil,
queue: nil
) { [weak self] notification in
guard let self = self,
let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else {
return
}
let device: [String: String] = [
"name": accessory.name,
"address": accessory.serialNumber
]
self.stateCallback?(.deviceConnected, ["device": device])
self?.handleAudioRouteChange(notification)
}
// 观察配件断开
accessoryDisconnectionObserver = notificationCenter.addObserver(
forName: NSNotification.Name.EAAccessoryDidDisconnect,
object: nil,
queue: nil
) { [weak self] notification in
guard let self = self,
let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else {
return
// 激活音频会话
activateAudioSession()
}
}
// 处理音频路由变化
private func handleAudioRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
// 新音频设备已连接
let audioDevices = getConnectedAudioDevices()
for device in audioDevices {
stateCallback?(.deviceConnected, ["device": device])
}
case .oldDeviceUnavailable:
// 获取断开之前的路由
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where isBluetoothOutput(output.portType) {
let device: [String: String] = [
"name": output.portName,
"address": cleanDeviceAddress(output.uid)
]
stateCallback?(.deviceDisconnected, ["device": device])
}
let device: [String: String] = [
"name": accessory.name,
"address": accessory.serialNumber
]
self.stateCallback?(.deviceDisconnected, ["device": device])
}
// 开始监视配件变化
EAAccessoryManager.shared().registerForLocalNotifications()
default:
break
}
}
// 激活音频会话
private func activateAudioSession() {
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default)
try audioSession.setActive(true)
} catch {
print("设置音频会话失败: \(error.localizedDescription)")
}
}
@ -129,48 +221,36 @@ class ClassicBluetoothHelper: NSObject {
if isMonitoring {
isMonitoring = false
// 移除蓝牙中央管理器代理
centralManager?.delegate = nil
// 注销配件通知观察者
if let observer = accessoryConnectionObserver {
// 移除音频路由观察者
if let observer = routeChangeObserver {
NotificationCenter.default.removeObserver(observer)
accessoryConnectionObserver = nil
routeChangeObserver = nil
}
if let observer = accessoryDisconnectionObserver {
NotificationCenter.default.removeObserver(observer)
accessoryDisconnectionObserver = nil
}
// 停止监视配件变化
EAAccessoryManager.shared().unregisterForLocalNotifications()
// 关闭音频会话
deactivateAudioSession()
}
// 清除回调
stateCallback = nil
}
// 关闭音频会话
private func deactivateAudioSession() {
do {
try AVAudioSession.sharedInstance().setActive(false)
} catch {
print("关闭音频会话失败: \(error.localizedDescription)")
}
}
/// 报告当前蓝牙状态
func reportCurrentBluetoothState() {
// 如果centralManager未初始化,不触发状态报告
guard let centralManager = centralManager else { return }
let state: BluetoothState
switch centralManager.state {
case .poweredOn:
state = .on
case .poweredOff:
state = .off
case .resetting:
state = .turningOff // 最接近的近似值
case .unauthorized, .unsupported:
state = .off // 功能上等同于关闭
default:
state = .unknown
}
stateCallback?(.stateChanged, ["state": state.rawValue])
// 直接调用delegate方法保持状态处理一致性
centralManagerDidUpdateState(centralManager)
}
}
@ -179,19 +259,47 @@ extension ClassicBluetoothHelper: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
let state: BluetoothState
// 添加详细日志输出
print("iOS蓝牙状态原始值: \(central.state.rawValue)")
switch central.state {
case .poweredOn:
print("蓝牙状态: 已开启(.poweredOn)")
state = .on
case .poweredOff:
print("蓝牙状态: 已关闭(.poweredOff)")
state = .off
case .unauthorized:
print("蓝牙状态: 未授权(.unauthorized)")
state = .unauthorized
case .unsupported:
print("蓝牙状态: 不支持(.unsupported)")
state = .unsupported
case .resetting:
state = .turningOff
case .unauthorized, .unsupported:
state = .off
default:
print("蓝牙状态: 重置中(.resetting)")
state = .resetting
case .unknown:
print("蓝牙状态: 未知(.unknown)")
state = .unknown
@unknown default:
print("蓝牙状态: 未处理的新状态(\(central.state.rawValue))")
state = .unknown
}
stateCallback?(.stateChanged, ["state": state.rawValue])
print("向Flutter端报告的蓝牙状态: \(state.rawValue)")
// 仅发送兼容的状态(为保持向后兼容,只发送on/off/unknown)
let reportState: String
switch state {
case .on:
reportState = BluetoothState.on.rawValue
case .off, .unauthorized, .unsupported:
// 这些状态对用户来说都意味着蓝牙不可用
reportState = BluetoothState.off.rawValue
default:
reportState = BluetoothState.unknown.rawValue
}
stateCallback?(.stateChanged, ["state": reportState])
}
}

136
local_plugins/classic_bluetooth/ios/Classes/FileLogger.swift

@ -1,136 +0,0 @@
import Foundation
import os.log
/// iOS端的文件日志助手类,提供与Android端FileLogger类似的功能
class FileLogger {
private static let logQueue = DispatchQueue(label: "com.yunqiinnovation.classic_bluetooth.logger", qos: .background)
private static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter
}()
/// 日志类型
enum LogLevel: String {
case debug = "DEBUG"
case info = "INFO"
case warning = "WARNING"
case error = "ERROR"
}
/// 输出调试日志
/// - Parameters:
/// - tag: 标签
/// - message: 日志消息
static func d(_ tag: String, _ message: String) {
log(level: .debug, tag: tag, message: message)
}
/// 输出信息日志
/// - Parameters:
/// - tag: 标签
/// - message: 日志消息
static func i(_ tag: String, _ message: String) {
log(level: .info, tag: tag, message: message)
}
/// 输出警告日志
/// - Parameters:
/// - tag: 标签
/// - message: 日志消息
static func w(_ tag: String, _ message: String) {
log(level: .warning, tag: tag, message: message)
}
/// 输出错误日志
/// - Parameters:
/// - tag: 标签
/// - message: 日志消息
/// - error: 可选的错误对象
static func e(_ tag: String, _ message: String, _ error: Error? = nil) {
var fullMessage = message
if let error = error {
fullMessage += " - Error: \(error.localizedDescription)"
}
log(level: .error, tag: tag, message: fullMessage)
}
/// 输出日志
/// - Parameters:
/// - level: 日志级别
/// - tag: 标签
/// - message: 日志消息
private static func log(level: LogLevel, tag: String, message: String) {
let timestamp = dateFormatter.string(from: Date())
let logMessage = "[\(timestamp)] [\(level.rawValue)] [\(tag)]: \(message)"
// 输出到控制台
print(logMessage)
// 异步写入日志文件
logQueue.async {
writeToLogFile(logMessage)
}
// 使用系统日志
if #available(iOS 10.0, *) {
let osLog = OSLog(subsystem: "com.yunqiinnovation.classic_bluetooth", category: tag)
let osLogType: OSLogType
switch level {
case .debug:
osLogType = .debug
case .info:
osLogType = .info
case .warning:
osLogType = .default
case .error:
osLogType = .error
}
os_log("%{public}@", log: osLog, type: osLogType, message)
}
}
/// 将日志写入文件
/// - Parameter message: 日志消息
private static func writeToLogFile(_ message: String) {
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return
}
let logDirectoryURL = documentsDirectory.appendingPathComponent("Logs")
let dateString = DateFormatter().with { $0.dateFormat = "yyyy-MM-dd" }.string(from: Date())
let logFileURL = logDirectoryURL.appendingPathComponent("bluetooth_\(dateString).log")
do {
// 创建日志文件夹
try FileManager.default.createDirectory(at: logDirectoryURL, withIntermediateDirectories: true)
// 如果文件不存在,创建文件
if !FileManager.default.fileExists(atPath: logFileURL.path) {
FileManager.default.createFile(atPath: logFileURL.path, contents: nil)
}
// 追加日志内容
if let fileHandle = FileHandle(forWritingAtPath: logFileURL.path) {
fileHandle.seekToEndOfFile()
if let data = "\(message)\n".data(using: .utf8) {
fileHandle.write(data)
}
fileHandle.closeFile()
}
} catch {
print("写入日志文件失败: \(error.localizedDescription)")
}
}
}
// MARK: - DateFormatter 扩展
extension DateFormatter {
func with(_ block: (DateFormatter) -> Void) -> DateFormatter {
block(self)
return self
}
}

42
local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift

@ -1,6 +1,7 @@
import Flutter
import UIKit
import ExternalAccessory
import AVFoundation
// 这个文件是为了满足Flutter插件的命名约定
// 实际实现在ClassicBluetoothPlugin.swift中
@ -22,6 +23,9 @@ public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin {
instance.methodChannel = channel
instance.eventChannel = eventChannel
// 记录插件初始化
print("经典蓝牙插件初始化成功")
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@ -36,18 +40,37 @@ public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin {
}
private func getConnectedDevices(result: @escaping FlutterResult) {
// 先检查蓝牙状态
if !bluetoothHelper.isBluetoothEnabled() {
print("蓝牙未启用,无法获取设备")
result([])
return
}
bluetoothHelper.getConnectedDevices { devices, error in
if let error = error {
print("获取设备失败: \(error.localizedDescription)")
result(FlutterError(code: "BLUETOOTH_ERROR", message: error.localizedDescription, details: nil))
return
}
result(devices)
print("获取到 \(devices.count) 个蓝牙设备")
for device in devices {
if let name = device["name"], let address = device["address"], let type = device["type"] {
print("设备: \(name), 地址: \(address), 类型: \(type)")
}
}
// 转换为Flutter可用的格式
let resultList = devices.map { device -> [String: String] in
// 仅保留Flutter端需要的字段
return [
"name": device["name"] ?? "未知设备",
"address": device["address"] ?? "未知地址"
]
}
result(resultList)
}
}
@ -71,6 +94,7 @@ extension SwiftClassicBluetoothPlugin: FlutterStreamHandler {
switch eventType {
case .stateChanged:
if let state = data["state"] as? String {
print("蓝牙状态变化: \(state)")
self.sendEvent([
"type": "stateChanged",
"state": state,
@ -79,17 +103,29 @@ extension SwiftClassicBluetoothPlugin: FlutterStreamHandler {
}
case .deviceConnected:
if let device = data["device"] as? [String: String] {
print("设备连接: \(device["name"] ?? "未知") [\(device["address"] ?? "未知")]")
// 只保留Flutter端需要的字段
let deviceData: [String: String] = [
"name": device["name"] ?? "未知设备",
"address": device["address"] ?? "未知地址"
]
self.sendEvent([
"type": "deviceConnected",
"device": device,
"device": deviceData,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
])
}
case .deviceDisconnected:
if let device = data["device"] as? [String: String] {
print("设备断开: \(device["name"] ?? "未知") [\(device["address"] ?? "未知")]")
// 只保留Flutter端需要的字段
let deviceData: [String: String] = [
"name": device["name"] ?? "未知设备",
"address": device["address"] ?? "未知地址"
]
self.sendEvent([
"type": "deviceDisconnected",
"device": device,
"device": deviceData,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
])
}

2
local_plugins/open_ai_service/android/src/main/AndroidManifest.xml

@ -4,6 +4,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<application>
<!-- 插件不需要额外的组件声明 -->

421
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt

@ -11,6 +11,7 @@ import android.os.Bundle
import android.provider.ContactsContract
import android.provider.AlarmClock
import android.provider.CalendarContract
import android.provider.MediaStore
import android.provider.Settings
import android.telephony.SmsManager
import android.util.Log
@ -22,6 +23,13 @@ import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers
import android.Manifest
import android.content.ContentValues
import android.content.ContentUris
import androidx.core.content.ContextCompat
import android.app.ActivityManager
import android.app.Application
import android.os.Process
/**
* 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能
*/
@ -67,6 +75,9 @@ class SystemFunctionHandler(private val context: Context) {
registerMediaPreviousFunction(client)
registerMediaNextFunction(client)
// 注册打开录音机功能
registerOpenRecorderFunction(client)
Log.d(TAG, "所有系统功能注册完成")
} catch (e: Exception) {
Log.e(TAG, "注册系统功能失败: ${e.message}", e)
@ -409,6 +420,30 @@ class SystemFunctionHandler(private val context: Context) {
Log.d(TAG, "已注册媒体下一首功能")
}
/**
* 注册打开录音机功能
*/
private fun registerOpenRecorderFunction(client: MCPClient) {
client.registerLocalFunction(
"open_recorder",
"打开系统录音机并开始录音",
JSONObject("""
{
"type": "object",
"properties": {},
"required": []
}
"""),
object : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
val result = openRecorder()
return "{\"result\": \"${result.message}\"}"
}
}
)
Log.d(TAG, "已注册打开录音机功能")
}
/**
* 控制媒体播放
* @param action 操作类型:"play", "pause", "previous", "next"
@ -555,6 +590,93 @@ class SystemFunctionHandler(private val context: Context) {
)
}
/**
* 检查权限并根据应用状态采取不同行为
* @param permission 需要检查的权限
* @param permissionName 权限名称(用于显示给用户)
* @param functionName 功能名称(用于日志和提示)
* @return 是否拥有权限
*/
private fun checkPermissionAndGuide(permission: String, permissionName: String, functionName: String): Boolean {
if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "缺少${permissionName}权限,无法执行${functionName}")
// 检查应用是否在前台
if (isAppInForeground()) {
// 应用在前台,尝试直接弹出权限申请框
try {
// 首先尝试发送广播通知DeepSound应用弹出权限申请对话框
val intent = Intent("com.yunqiinnovation.deepsound.action.REQUEST_PERMISSION").apply {
setPackage(context.packageName)
putExtra("permission", permission)
putExtra("permissionName", permissionName)
putExtra("functionName", functionName)
addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
}
context.sendBroadcast(intent)
// 由于插件无法直接请求权限(需要Activity),上面的广播会让主应用来处理权限请求
// 这里设置一个短暂延迟,让用户有时间响应权限请求
Thread.sleep(500)
// 再次检查权限是否已授予(以防用户立即授予)
if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) {
return true
}
} catch (e: Exception) {
Log.e(TAG, "请求权限失败: ${e.message}", e)
}
// 如果权限仍未授予,引导用户到权限管理页面
try {
// 尝试跳转到应用内的权限管理页面
val intent = Intent("com.yunqiinnovation.deepsound.action.PERMISSIONS_SETTINGS").apply {
setPackage(context.packageName)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
// 如果应用内权限页面不可用,则跳转到系统应用设置页面
val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(settingsIntent)
}
} catch (e: Exception) {
Log.e(TAG, "打开权限设置页面失败: ${e.message}", e)
}
}
// 应用在后台或权限请求失败,只返回文字提示
return false
}
return true
}
/**
* 检查应用是否在前台运行
* @return 应用是否在前台
*/
private fun isAppInForeground(): Boolean {
try {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val appProcesses = activityManager.runningAppProcesses ?: return false
val packageName = context.packageName
for (appProcess in appProcesses) {
if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
&& appProcess.processName == packageName) {
return true
}
}
} catch (e: Exception) {
Log.e(TAG, "检查应用状态失败: ${e.message}", e)
}
return false
}
/**
* 发送短信
* @param contact 联系人(名称或电话号码)
@ -565,6 +687,14 @@ class SystemFunctionHandler(private val context: Context) {
Log.d(TAG, "发送短信给 $contact: $message")
try {
// 检查发送短信权限
if (!checkPermissionAndGuide(Manifest.permission.SEND_SMS, "发送短信", "发送短信")) {
return Result(
success = false,
message = "需要短信权限才能发送短信,请在权限管理中开启短信权限"
)
}
val phoneNumber = getPhoneNumberForContact(contact)
if (phoneNumber.isEmpty()) {
return Result(
@ -599,6 +729,14 @@ class SystemFunctionHandler(private val context: Context) {
Log.d(TAG, "拨打电话给 $contact")
try {
// 检查拨打电话权限
if (!checkPermissionAndGuide(Manifest.permission.CALL_PHONE, "拨打电话", "拨打电话")) {
return Result(
success = false,
message = "需要电话权限才能拨打电话,请在权限管理中开启电话权限"
)
}
val phoneNumber = getPhoneNumberForContact(contact)
if (phoneNumber.isEmpty()) {
return Result(
@ -688,6 +826,11 @@ class SystemFunctionHandler(private val context: Context) {
return contact.replace(Regex("[^+\\d]"), "")
}
// 检查读取联系人权限
if (!checkPermissionAndGuide(Manifest.permission.READ_CONTACTS, "读取联系人", "访问联系人")) {
return ""
}
// 从联系人数据库查询
try {
val contentResolver = context.contentResolver
@ -751,55 +894,62 @@ class SystemFunctionHandler(private val context: Context) {
)
}
// 使用系统日历应用创建提醒
val intent = Intent(Intent.ACTION_INSERT).apply {
data = CalendarContract.Events.CONTENT_URI
putExtra(CalendarContract.Events.TITLE, title)
putExtra(CalendarContract.Events.DESCRIPTION, content)
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, time)
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, time + 3600000) // 默认1小时
putExtra(CalendarContract.Events.HAS_ALARM, 1)
putExtra(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
// 检查日历权限
if (!checkPermissionAndGuide(Manifest.permission.WRITE_CALENDAR, "日历写入", "设置提醒")) {
return Result(
success = false,
message = "需要日历权限才能创建提醒,请在权限管理中开启日历权限"
)
}
// 直接使用ContentProvider写入日历
val endTime = time + 3600000 // 默认1小时后结束
// 获取默认日历ID
val calendarId = getDefaultCalendarId()
if (calendarId == -1L) {
Log.w(TAG, "未找到默认日历")
return tryCreateAlarm(title, content, timeStr)
}
// 创建事件
val eventValues = ContentValues().apply {
put(CalendarContract.Events.CALENDAR_ID, calendarId)
put(CalendarContract.Events.TITLE, title)
put(CalendarContract.Events.DESCRIPTION, content)
put(CalendarContract.Events.DTSTART, time)
put(CalendarContract.Events.DTEND, endTime)
put(CalendarContract.Events.HAS_ALARM, 1)
put(CalendarContract.Events.EVENT_TIMEZONE, java.util.TimeZone.getDefault().id)
}
// 检查是否有处理该Intent的应用
val calendarActivity = intent.resolveActivity(context.packageManager)
if (calendarActivity != null) {
context.startActivity(intent)
val eventUri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, eventValues)
if (eventUri == null) {
Log.e(TAG, "插入日历事件失败")
return tryCreateAlarm(title, content, timeStr)
}
// 获取事件ID
val eventId = ContentUris.parseId(eventUri)
// 添加提醒(15分钟前提醒)
val reminderValues = ContentValues().apply {
put(CalendarContract.Reminders.EVENT_ID, eventId)
put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT)
put(CalendarContract.Reminders.MINUTES, 15) // 提前15分钟提醒
}
val reminderUri = context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, reminderValues)
if (reminderUri != null) {
return Result(
success = true,
message = "已创建提醒: $title, 时间: $timeStr"
message = "已成功创建提醒: $title, 时间: $timeStr"
)
} else {
// 备选方案:如果没有日历应用,尝试创建闹钟
try {
// 解析小时和分钟
val timeParts = timeStr.split(" ")[1].split(":")
val hour = timeParts[0].toInt()
val minute = timeParts[1].toInt()
val alarmIntent = Intent(AlarmClock.ACTION_SET_ALARM)
alarmIntent.putExtra(AlarmClock.EXTRA_MESSAGE, "$title: $content")
alarmIntent.putExtra(AlarmClock.EXTRA_HOUR, hour)
alarmIntent.putExtra(AlarmClock.EXTRA_MINUTES, minute)
alarmIntent.putExtra(AlarmClock.EXTRA_SKIP_UI, false)
alarmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
if (alarmIntent.resolveActivity(context.packageManager) != null) {
context.startActivity(alarmIntent)
return Result(
success = true,
message = "已创建闹钟提醒: $title, 时间: $timeStr"
)
}
} catch (e: Exception) {
Log.e(TAG, "创建闹钟失败: ${e.message}", e)
}
// 仍然创建了事件,但没有提醒
return Result(
success = false,
message = "未找到可用的日历或闹钟应用"
success = true,
message = "已创建日历事件,但未设置提醒: $title"
)
}
} catch (e: Exception) {
@ -811,6 +961,69 @@ class SystemFunctionHandler(private val context: Context) {
}
}
/**
* 获取默认日历ID
*/
private fun getDefaultCalendarId(): Long {
val projection = arrayOf(CalendarContract.Calendars._ID)
var calendarId = -1L
try {
val uri = CalendarContract.Calendars.CONTENT_URI
val selection = "(${CalendarContract.Calendars.VISIBLE} = 1) AND (${CalendarContract.Calendars.IS_PRIMARY} = 1)"
context.contentResolver.query(uri, projection, selection, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
calendarId = cursor.getLong(0)
} else {
// 如果没有找到主日历,尝试找任何可见的日历
context.contentResolver.query(uri, projection, "${CalendarContract.Calendars.VISIBLE} = 1", null, null)?.use { c ->
if (c.moveToFirst()) {
calendarId = c.getLong(0)
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "获取默认日历ID失败: ${e.message}", e)
}
return calendarId
}
/**
* 尝试创建闹钟提醒(备选方案)
*/
private fun tryCreateAlarm(title: String, content: String, timeStr: String): Result {
try {
// 解析小时和分钟
val timeParts = timeStr.split(" ")[1].split(":")
val hour = timeParts[0].toInt()
val minute = timeParts[1].toInt()
val alarmIntent = Intent(AlarmClock.ACTION_SET_ALARM)
alarmIntent.putExtra(AlarmClock.EXTRA_MESSAGE, "$title: $content")
alarmIntent.putExtra(AlarmClock.EXTRA_HOUR, hour)
alarmIntent.putExtra(AlarmClock.EXTRA_MINUTES, minute)
alarmIntent.putExtra(AlarmClock.EXTRA_SKIP_UI, true) // 尝试跳过UI
alarmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
if (alarmIntent.resolveActivity(context.packageManager) != null) {
context.startActivity(alarmIntent)
return Result(
success = true,
message = "已创建闹钟提醒: $title, 时间: $timeStr"
)
}
} catch (e: Exception) {
Log.e(TAG, "创建闹钟失败: ${e.message}", e)
}
return Result(
success = false,
message = "未找到可用的日历或闹钟应用"
)
}
/**
* 获取当前时间
* @param format 时间格式(可选)
@ -892,22 +1105,10 @@ class SystemFunctionHandler(private val context: Context) {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
// 检查位置权限
val fineLocationPermission = android.Manifest.permission.ACCESS_FINE_LOCATION
val coarseLocationPermission = android.Manifest.permission.ACCESS_COARSE_LOCATION
val hasFineLocationPermission = context.checkPermission(
fineLocationPermission, android.os.Process.myPid(), android.os.Process.myUid()
) == PackageManager.PERMISSION_GRANTED
val hasCoarseLocationPermission = context.checkPermission(
coarseLocationPermission, android.os.Process.myPid(), android.os.Process.myUid()
) == PackageManager.PERMISSION_GRANTED
if (!hasFineLocationPermission && !hasCoarseLocationPermission) {
Log.d(TAG, "没有位置权限")
if (!checkPermissionAndGuide(Manifest.permission.ACCESS_FINE_LOCATION, "位置信息", "获取位置")) {
return@withContext Result(
success = false,
message = "需要位置权限才能获取当前位置"
message = "需要位置权限才能获取当前位置,请在权限管理中开启位置权限"
)
}
@ -916,6 +1117,15 @@ class SystemFunctionHandler(private val context: Context) {
!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
Log.d(TAG, "位置服务未启用")
// 引导用户打开位置服务
try {
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
} catch (e: Exception) {
Log.e(TAG, "打开位置设置失败: ${e.message}", e)
}
return@withContext Result(
success = false,
message = "位置服务未启用,请打开GPS或网络定位"
@ -962,6 +1172,105 @@ class SystemFunctionHandler(private val context: Context) {
}
}
/**
* 打开系统录音机并尝试自动开始录音
* @return 操作结果
*/
fun openRecorder(): Result {
Log.d(TAG, "打开系统录音机并尝试自动开始录音")
try {
// 创建用于启动录音机应用的Intent
val recordIntent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
recordIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
// 添加额外参数尝试自动开始录音(不是所有应用都支持)
recordIntent.putExtra("android.intent.extra.quickCapture", true)
// 检查是否有应用能处理此Intent
if (recordIntent.resolveActivity(context.packageManager) != null) {
context.startActivity(recordIntent)
return Result(
success = true,
message = "已打开录音机,尝试自动开始录音"
)
} else {
// 尝试使用备用方法,针对不同厂商应用添加特定参数
val commonRecorderIntents = mapOf(
"com.android.soundrecorder" to Intent().setClassName("com.android.soundrecorder", "com.android.soundrecorder.SoundRecorder").setAction("android.intent.action.RECORD_SOUND"),
"com.sec.android.app.voicenote" to Intent().setClassName("com.sec.android.app.voicenote", "com.sec.android.app.voicenote.Main").putExtra("launch_mode", "record"),
"com.miui.soundrecorder" to Intent().setClassName("com.miui.soundrecorder", "com.miui.soundrecorder.SoundRecorderActivity").putExtra("startOnBoot", true),
"com.huawei.recorder" to Intent().setClassName("com.huawei.recorder", "com.huawei.recorder.activity.MainActivity").putExtra("autoRecord", true),
"com.google.android.apps.recorder" to Intent().setClassName("com.google.android.apps.recorder", "com.google.android.apps.recorder.ui.RecorderActivity").putExtra("start_recording", true)
)
for ((packageName, intent) in commonRecorderIntents) {
try {
if (isPackageInstalled(packageName, context.packageManager)) {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
return Result(
success = true,
message = "已打开录音应用并尝试自动开始录音"
)
}
} catch (e: Exception) {
Log.e(TAG, "尝试打开录音应用 $packageName 失败: ${e.message}")
// 继续尝试下一个应用
}
}
// 如果特定Intent都失败,回退到原来的方法
val commonRecorderPackages = listOf(
"com.android.soundrecorder",
"com.sec.android.app.voicenote",
"com.miui.soundrecorder",
"com.huawei.recorder",
"com.google.android.apps.recorder"
)
for (packageName in commonRecorderPackages) {
try {
val packageIntent = context.packageManager.getLaunchIntentForPackage(packageName)
if (packageIntent != null) {
packageIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
// 尝试添加通用的自动开始录音参数
packageIntent.putExtra("android.intent.extra.quickCapture", true)
context.startActivity(packageIntent)
return Result(
success = true,
message = "已打开录音应用,请手动开始录音"
)
}
} catch (e: Exception) {
Log.e(TAG, "尝试打开录音应用 $packageName 失败: ${e.message}")
}
}
return Result(
success = false,
message = "未找到录音应用"
)
}
} catch (e: Exception) {
Log.e(TAG, "打开录音机失败: ${e.message}", e)
return Result(
success = false,
message = "打开录音机失败: ${e.message}"
)
}
}
// 检查应用是否已安装
private fun isPackageInstalled(packageName: String, packageManager: PackageManager): Boolean {
return try {
packageManager.getPackageInfo(packageName, 0)
true
} catch (e: Exception) {
false
}
}
/**
* 操作结果数据类
*/

BIN
local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock

Binary file not shown.

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

Binary file not shown.

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

@ -1,2 +1,2 @@
#Sat May 17 17:27:45 IST 2025
#Sun May 18 21:39:38 IST 2025
gradle.version=8.10

45
test.md

@ -0,0 +1,45 @@
请帮我生成一个基于 GetX 最佳实践的 Flutter 应用项目骨架。要求如下:
1. 目录结构
- lib/
- main.dart
- app/
- data/
- providers/ # 网络请求、API 客户端(使用 GetConnect)
- repositories/ # Repository 层,封装业务数据源
- services/ # 全局服务(如 ThemeService、AuthService)
- modules/
- home/ # 示例模块
- bindings/ # HomeBinding,注册 Controller、Service
- controllers/ # HomeController
- models/ # HomeModel 数据类
- views/ # HomePage 页面
- routes/
- app_routes.dart # 路由常量
- app_pages.dart # GetPage 列表,包括中间件示例
- translations/ # 多语言文件(en_US、zh_CN)和 Translations 实现
- widgets/ # 公共 Widget(Loading、ErrorView)
- utils/ # 日志 Logger、网络拦截器等工具函数
2. main.dart
- 使用 GetMaterialApp
- 配置 initialRoute、getPages、theme、darkTheme、locale、translations
3. 依赖注入(Bindings)
- 在 app/modules/**/bindings 中,用 Get.lazyPut、Get.putAsync、Get.create 分别示例单例、异步初始化和工厂模式
4. 状态管理
- 在 Controller 中使用 Rx<T> + .obs
- 在视图中用 Obx、GetX、GetBuilder
- 在 Controller 的 onInit、onReady、onClose 中写注释说明生命周期
5. 路由管理
- 在 app/routes/app_routes.dart 定义路由名称常量
- 在 app/routes/app_pages.dart 用 GetPage 配置路由、binding
8. 架构模式
- 强调 “Repository → Service → Controller → View” 分层
- 各层代码注释职责
- 代码风格统一、注释清晰
Loading…
Cancel
Save