diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 44c9dd0dc..20d5ead25 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -5,7 +5,6 @@ repositories { mavenCentral() flatDir { dirs(project(":spotify_sdk").projectDir.resolve("libs")) - dirs(project(":jl_ota").projectDir.resolve("libs")) } } diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index fa5282d77..cf7591c68 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -39,7 +39,7 @@ include(":ble_service") include(":classic_bluetooth") include(":agent_service") include(":speech") -include(":open_ai_service") +include(":chat_api") // 设置azure_speech项目的路径 project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") @@ -50,4 +50,4 @@ project(":ble_service").projectDir = file("../local_plugins/ble_service/android" project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android") project(":agent_service").projectDir = file("../local_plugins/agent_service/android") project(":speech").projectDir = file("../local_plugins/speech/android") -project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android") \ No newline at end of file +project(":chat_api").projectDir = file("../local_plugins/chat_api/android") \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 9a7b93fa1..b7e7fc98f 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -123,9 +123,9 @@ NSBluetoothPeripheralUsageDescription 需要蓝牙权限用于连接和管理音频设备 NSLocationAlwaysAndWhenInUseUsageDescription - 需要位置权限用于搜索附近的蓝牙设备 + 语音助手需要位置权限来为您提供基于位置的信息和服务 NSLocationWhenInUseUsageDescription - 需要位置权限用于搜索附近的蓝牙设备 + 语音助手需要位置权限来为您提供基于位置的信息和服务 NSMicrophoneUsageDescription 需要麦克风权限用于语音识别和录音功能 NSCalendarsUsageDescription diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 1b472e8ab..4fd99d91e 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -159,7 +159,7 @@ class BleManager extends GetxService { final state = event['state'] as int; final oldState = _connectionState.value; _connectionState.value = state; - + Logger.d('BleManager', '🔄 BLE状态更新: $oldState → $state'); Logger.d('BleManager', '📡 通知监听器状态变化: ${_connectionState.value}'); @@ -179,6 +179,19 @@ class BleManager extends GetxService { state == BleConnectionState.error) { Logger.info('BLE设备已断开或发生错误'); _isConnected.value = false; + // 更新设备信息 + _updateDeviceInfoIfNeeded( + batteryLevel: 0, + leftEarBattery: 0, + rightEarBattery: 0, + caseBattery: 0, + leftEarCharging: false, + rightEarCharging: false, + caseCharging: false, + firmwareVersion: '', + firmwareProductId: '', + firmwareProductColor: '', + ); } } @@ -418,7 +431,8 @@ class BleManager extends GetxService { } else { Logger.warning('电池信息获取指令发送失败'); } - + // 添加延迟 + await Future.delayed(const Duration(seconds: 1)); // 获取设备版本信息 final versionResult = await _bleService.getDeviceVersion(); if (versionResult) { @@ -426,7 +440,8 @@ class BleManager extends GetxService { } else { Logger.warning('版本信息获取指令发送失败'); } - + // 添加延迟 + await Future.delayed(const Duration(seconds: 1)); // 获取产品id final productIdResult = await _bleService.getProductId(); if (productIdResult) { @@ -655,7 +670,7 @@ class BleManager extends GetxService { } // ==================== 配对相关方法 ==================== - + /// 检查设备是否支持Companion Device Manager Future isCompanionSupported() async { try { @@ -732,6 +747,16 @@ class BleManager extends GetxService { } } + /// 清除BLE配对设备 + Future clearPairedDevices() async { + try { + return await _bleService.clearAssociations(); + } catch (e) { + Logger.error('清除BLE配对设备失败: ${e.toString()}'); + return false; + } + } + /// 注册服务 static Future init() async { if (!Get.isRegistered()) { diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index c9c827551..8d0f3653b 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -7,82 +7,148 @@ import '../model/ota_upgrade_model.dart'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:jl_ota/jl_ota.dart'; import 'package:ota/ota.dart'; import '../../../data/services/ble_manager.dart'; class OtaUpgradeController extends GetxController { + static const int NOT_STARTED = -1; // 未开始 + static const int VALIDATION_FILE = 0; // 验证文件 + static const int ONUPGRADE = 1; // 升级中 + static const int START_UPGRADING = 2; // 开始升级 + static const int UPGRADE_SUCCESS = 3; // 升级成功 + static const int UPGRADE_FAILED = 4; // 升级失败 + static const int CONNECTION_DISCONNECTION = 5; // 连接断开 + static const int CONNECTION_SUCCESS = 6; // 连接成功 + static const int DOWNLOADING_UP_FILES = 7; // 下载升级文件 + +// 错误信息,用于存储错误提示 final RxString errorMessage = ''.obs; - final RxBool isFetchingDeviceInfo = false.obs; - final Dio _dio = Dio(); + +// 最新版本号 final RxString latestVersion = ''.obs; + +// 当前设备固件版本号 final RxString currentVersion = ''.obs; + +// 更新包地址,用于下载更新文件 String updatepackageaddress = ''; + +// 下载进度,范围为 0.0 到 1.0 final RxDouble downloadProgress = 0.0.obs; - final RxBool isUpdating = false.obs; + +// 是否有可用更新的状态 final isUpdateAvailable = true.obs; - final RxDouble progress = 0.0.obs; - var jlota = Ota(); - // 蓝牙服务 +// 当前升级进度,范围为 0.0 到 1.0 + // final RxDouble progress = 0.0.obs; + +// OTA 单例实例,用于处理升级相关逻辑 + final jlota = Ota.instance; + +// 是否正在下载更新包的状态 + final RxBool isDownloading = false.obs; + +// 蓝牙管理服务,用于获取设备信息和通信 final _bleManager = Get.find(); + final maxRetryAttempts = 3; // 最大重试次数 + int currentRetryCount = 0; + + final RxBool isFetchingDeviceInfo = true.obs; + + final RxDouble progress = 0.0.obs; // 控制器自己的可观察属性 + final RxInt type = OtaEvent.NOT_STARTED.obs; + final RxBool isUpgrade = false.obs; @override void onInit() { super.onInit(); _getCurrentVersion(); } + @override + void onClose() { + jlota.onOtaClose(); + isUpgrade.value = false; + super.onClose(); + } + // 添加重试方法 void retryConnection() { errorMessage.value = ''; _getCurrentVersion(); } + String getStatusMessage(int type) { + switch (type) { + case NOT_STARTED: // -1 + return "未开始".tr; + case VALIDATION_FILE: // 0 + return "验证文件中".tr; + case ONUPGRADE: // 1 + return "升级中".tr; + case START_UPGRADING: // 2 + return "开始升级".tr; + case UPGRADE_SUCCESS: // 3 + return "升级成功".tr; + case UPGRADE_FAILED: // 4 + return "升级失败".tr; + case CONNECTION_DISCONNECTION: // 5 + return "连接断开".tr; + case CONNECTION_SUCCESS: // 6 + return "连接成功".tr; + case DOWNLOADING_UP_FILES: // 7 (新增) + return "下载升级文件".tr; + default: + return "未知状态".tr; // 更安全的默认值 + } + } + // 添加更新方法 Future startUpdate() async { try { - isUpdating.value = true; checkUpdate(); // 实现更新逻辑... } catch (e) { errorMessage.value = e.toString(); Get.snackbar('更新失败', e.toString()); - } finally { - isUpdating.value = false; } } Future _getCurrentVersion() async { + _listenToEvents(); try { - jlota.getPlatformVersion(); + isFetchingDeviceInfo.value = true; + errorMessage.value = ''; // 添加获取设备信息的重试机制 DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; - // 第一次检查设备信息 - if (deviceInfo == null) { - // 显示加载提示 - isFetchingDeviceInfo.value = true; + // // 第一次检查设备信息 + // if (deviceInfo == null) { + // // 尝试重新获取设备信息 + // await _bleManager.fetchDeviceInfo(); + // } - // 尝试重新获取设备信息 - await _bleManager.fetchDeviceInfo(); - - // 等待后再次获取设备信息 + while (currentRetryCount < maxRetryAttempts) { deviceInfo = _bleManager.deviceInfo.value; - isFetchingDeviceInfo.value = false; - // 仍然为空则抛出异常 - if (deviceInfo == null) { - throw const FormatException('无法获取设备信息,请检查设备连接'); + // 检查信息有效性 + if (deviceInfo != null && + deviceInfo.firmwareVersion.isNotEmpty && + deviceInfo.firmwareProductId.isNotEmpty) { + break; } - } - // 检查固件信息有效性 - if (deviceInfo.firmwareVersion.isEmpty || + // 获取设备信息 + await _bleManager.fetchDeviceInfo(); + await Future.delayed(const Duration(seconds: 5)); + currentRetryCount++; + } + // 最终验证 + if (deviceInfo == null || + deviceInfo.firmwareVersion.isEmpty || deviceInfo.firmwareProductId.isEmpty) { - throw const FormatException('设备固件信息不完整'); + throw "获取设备信息失败,请检查连接"; } - // 正常流程 currentVersion.value = deviceInfo.firmwareVersion; print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}'); @@ -98,7 +164,8 @@ class OtaUpgradeController extends GetxController { final resp = OtaConfigResp.fromJson(response); print('getequipmentinfo,resp.systemversion: ${resp.systemversion}'); - latestVersion.value = "1"; + latestVersion.value = resp.systemversion!; + updatepackageaddress = resp.updatepackageaddress!; // 版本检查 @@ -141,9 +208,9 @@ class OtaUpgradeController extends GetxController { // 版本更新 Future checkUpdate() async { try { - if (latestVersion.value != "") + if (latestVersion.value != "") { _showUpdateDialog(latestVersion.value); - else { + } else { Get.snackbar('错误', '获取最新版本信息失败'); } } catch (e) { @@ -151,20 +218,28 @@ class OtaUpgradeController extends GetxController { } } - // 显示更新对话框 - void _showUpdateDialog(String latest) { + void _showUpdateDialog(String latest) async { + print('显示更新对话框'); Get.dialog( AlertDialog( title: Text('发现新版本 $latest'), content: Text('是否立即更新?'), actions: [ TextButton( - onPressed: () => Get.back(), + onPressed: () async { + print('取消按钮被点击'); + + Get.back(); + Future.delayed(Duration(milliseconds: 300)); + }, child: Text('取消'), ), TextButton( - onPressed: () { + onPressed: () async { + print('更新按钮被点击'); + Get.back(); + Future.delayed(Duration(milliseconds: 300)); startDownload(); }, child: Text('更新'), @@ -176,7 +251,7 @@ class OtaUpgradeController extends GetxController { // 修改后的startDownload方法 Future startDownload() async { - isUpdating.value = true; + isDownloading.value = true; int lastIndex = updatepackageaddress.lastIndexOf('/'); String fileName = updatepackageaddress.substring(lastIndex + 1); String directoryName = "ota_upgrades"; @@ -188,8 +263,7 @@ class OtaUpgradeController extends GetxController { headers: {'Accept-Encoding': 'gzip'}, )); - Get.snackbar('升级命令已发送', '开始下载...'); - + type.value = DOWNLOADING_UP_FILES; try { final Directory? downloadsDir = await getDownloadsDirectory(); if (downloadsDir == null) { @@ -218,14 +292,14 @@ class OtaUpgradeController extends GetxController { } } catch (e) { Get.snackbar('错误', '检查本地文件失败: $e'); - isUpdating.value = false; + return; } if (existingSameFile) { Get.snackbar('文件已存在', '跳过下载,直接更新'); - isUpdating.value = false; - _installApp(savePath); + + startUpgrading(savePath); return; } @@ -239,7 +313,7 @@ class OtaUpgradeController extends GetxController { } } catch (e) { Get.snackbar('错误', '删除旧文件失败: $e'); - isUpdating.value = false; + //isUpgrade.value = false; return; } @@ -250,59 +324,80 @@ class OtaUpgradeController extends GetxController { followRedirects: true, maxRedirects: 5, ); - - DateTime lastUpdate = DateTime.now(); + // 修改下载部分 await dio.download( updatepackageaddress, savePath, options: options, onReceiveProgress: (received, total) { - final now = DateTime.now(); - if (now.difference(lastUpdate).inMilliseconds > 1000) { - lastUpdate = now; - String progressMsg = total != -1 - ? '${(received / total * 100).toStringAsFixed(1)}%' - : '已下载:${(received / 1024 / 1024).toStringAsFixed(1)}MB'; - Get.snackbar( - '升级进度', - progressMsg, - duration: const Duration(seconds: 1), - ); + if (total != -1) { + downloadProgress.value = received / total; } }, ); - File file = File(savePath); if (await file.exists()) { Get.snackbar('下载完成', '文件已保存至: $savePath'); } else { Get.snackbar('错误', '文件未找到,请检查写入权限'); } - isUpdating.value = false; - _installApp(savePath); + isDownloading.value = false; + startUpgrading(savePath); + return; } on DioException catch (e) { Get.snackbar('下载失败', '错误: ${e.message}'); - isUpdating.value = false; } on FileSystemException catch (e) { Get.snackbar('存储失败', '无法写入文件: ${e.message}'); - isUpdating.value = false; } catch (e) { Get.snackbar('未知错误', e.toString()); - isUpdating.value = false; } } // 开始升级 - void _installApp(String path) async { - jlota.searchDevices(); - jlota.startUpgrade(); - // JlOtaPlugin() - // final File file = File(path); - // if (await file.exists()) { - // // OpenResult result = await OpenFile.open(path); - // // if (result.type != ResultType.done) { - // // Get.snackbar('错误', '安装失败: ${result.message}'); - // // } - // } + void startUpgrading(String path) async { + jlota.startUpgrading(path); + return; + // isUpgrade.value = false; + } + + // 停止升级 + void stopUpgrading() async { + jlota.onOtaClose(); + return; + // isUpgrade.value = false; + } + + void _listenToEvents() { + Ota.instance.eventStream.listen((event) { + if (event.event == 'onProgress' && + event.type != null && + event.progress != null) { + progress.value = event.progress!; + type.value = event.type!; + isUpgrade.value = true; // 开始升级 + } + if (event.event == 'onStartOTA') { + type.value = OtaEvent.START_UPGRADING; + isUpgrade.value = true; // 开始升级 + } + if (event.event == 'onStopOTA') { + type.value = OtaEvent.UPGRADE_SUCCESS; + isUpgrade.value = false; + Get.snackbar('升级成功', '请退出重新连接设备'); + _getCurrentVersion(); + } + if (event.event == 'onError') { + type.value = OtaEvent.UPGRADE_FAILED; + isUpgrade.value = false; + } + if (event.event == 'NOCONNECTION') { + type.value = OtaEvent.CONNECTION_DISCONNECTION; + isUpgrade.value = false; + } + if (event.event == 'CONNECTION_SUCCESS') { + type.value = OtaEvent.CONNECTION_SUCCESS; + isUpgrade.value = true; // 开始升级 + } + }); } } diff --git a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart index 4555f63c8..1bb1629bc 100644 --- a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart +++ b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart @@ -10,70 +10,125 @@ class OtaUpgradeView extends GetView { Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[100], - appBar: AppBar( - automaticallyImplyLeading: false, - centerTitle: true, - elevation: 0, - backgroundColor: isDarkMode ? Colors.black : Colors.white, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - size: 20.sp, - color: isDarkMode ? Colors.white : Colors.black, + return WillPopScope( + onWillPop: () async { + if (controller.isUpgrade.value) { + // 显示确认弹窗 + await Get.dialog( + AlertDialog( + title: Text('提示'), + content: Text('当前正在升级,退出将中断升级进程,确定要退出吗?'), + actions: [ + TextButton( + onPressed: () => Get.back(), // 关闭弹窗 + child: Text('取消'), + ), + TextButton( + onPressed: () { + controller.stopUpgrading(); + Get.back(); // 关闭弹窗 + Get.back(); // 返回上一页 + }, + child: Text('确定'), + ), + ], + ), + ); + return false; // 阻止默认返回 + } + return true; // 允许返回 + }, + child: Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[100], + appBar: AppBar( + automaticallyImplyLeading: false, + centerTitle: true, + elevation: 0, + backgroundColor: isDarkMode ? Colors.black : Colors.white, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + size: 20.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + onPressed: () { + if (controller.isUpgrade.value) { + // 显示确认弹窗 + Get.dialog( + AlertDialog( + title: Text('提示'), + content: Text('当前正在升级,退出将中断升级进程,确定要退出吗?'), + actions: [ + TextButton( + onPressed: () => Get.back(), // 关闭弹窗 + child: Text('取消'), + ), + TextButton( + onPressed: () { + controller.stopUpgrading(); + Get.back(); // 关闭弹窗 + Get.back(); // 返回上一页 + }, + child: Text('确定'), + ), + ], + ), + ); + } else { + Get.back(); // 正常返回 + } + }, ), - onPressed: () => Get.back(), - ), - title: Text( - '固件升级'.tr, - style: TextStyle( - fontSize: 20.sp, - color: isDarkMode ? Colors.white : Colors.black, + title: Text( + '固件升级'.tr, + style: TextStyle( + fontSize: 20.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), ), ), - ), - body: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 设备信息卡片 - Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - color: isDarkMode ? Colors.grey[900] : Colors.white, - child: Padding( - padding: EdgeInsets.all(20), - child: Column( - children: [ - _buildDeviceIcon(isDarkMode), - SizedBox(height: 20), - _buildVersionInfo(isDarkMode), - ], + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 设备信息卡片 + Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + color: isDarkMode ? Colors.grey[900] : Colors.white, + child: Padding( + padding: EdgeInsets.all(20), + child: Column( + children: [ + _buildDeviceIcon(isDarkMode), + SizedBox(height: 20), + _buildVersionInfo(isDarkMode), + ], + ), ), ), - ), - SizedBox(height: 20), + SizedBox(height: 20), - // 更新说明 - _buildUpdateNotes(isDarkMode), - SizedBox(height: 20), + // 更新说明 + _buildUpdateNotes(isDarkMode), + SizedBox(height: 20), - // 状态显示区域 - Obx(() { - if (controller.isFetchingDeviceInfo.value) { - return _buildLoadingIndicator(isDarkMode); - } - if (controller.errorMessage.value.isNotEmpty) { - return _buildErrorInfo(isDarkMode); - } - return _buildUpdateButton(isDarkMode); - }), - ], + // 状态显示区域 + Obx(() { + if (controller.isFetchingDeviceInfo.value) { + return _buildLoadingIndicator(isDarkMode); + } + if (controller.errorMessage.value.isNotEmpty) { + return _buildErrorInfo(isDarkMode); + } + return _buildUpdateButton(isDarkMode); + }), + ], + ), ), ), ), @@ -101,7 +156,7 @@ class OtaUpgradeView extends GetView { fontSize: 18, color: isDarkMode ? Colors.grey[300] : Colors.black)), SizedBox(width: 8), // 改为width - Obx(() => Text('v${controller.currentVersion}', + Obx(() => Text('V ${controller.currentVersion}', style: TextStyle( fontSize: 24, fontWeight: FontWeight.normal, @@ -117,11 +172,11 @@ class OtaUpgradeView extends GetView { fontSize: 18, color: isDarkMode ? Colors.grey[300] : Colors.black)), SizedBox(width: 8), // 改为width - Text('v${controller.latestVersion}', + Obx(() => Text('V ${controller.latestVersion}', style: TextStyle( fontSize: 24, fontWeight: FontWeight.normal, - color: isDarkMode ? Colors.green[300] : Colors.green)), + color: isDarkMode ? Colors.blue[300] : Colors.blue))), ], ), ], @@ -182,7 +237,7 @@ class OtaUpgradeView extends GetView { isDarkMode ? Colors.blue[300] : Colors.blue), ), SizedBox(height: 20), - Text('正在连接设备...', + Text('正在获取设备信息...', style: TextStyle(color: isDarkMode ? Colors.grey[300] : Colors.grey)) ], @@ -227,7 +282,7 @@ class OtaUpgradeView extends GetView { ); Widget _buildUpdateButton(bool isDarkMode) { - return controller.isUpdating.value + return controller.isUpgrade.value ? _buildProgressIndicator(isDarkMode) : Container( width: double.infinity, @@ -262,7 +317,7 @@ class OtaUpgradeView extends GetView { if (controller.isUpdateAvailable.value) ...[ SizedBox(height: 8), Text( - '建议连接WiFi并保持充电状态'.tr, + '取出双耳机,并保证电量均为30%以上的状态'.tr, style: TextStyle( fontSize: 14, color: isDarkMode ? Colors.grey[400] : Colors.grey[600], @@ -274,61 +329,67 @@ class OtaUpgradeView extends GetView { ); } - Widget _buildProgressIndicator(bool isDarkMode) => Container( - padding: EdgeInsets.all(20), - decoration: BoxDecoration( - color: - isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50], - borderRadius: BorderRadius.circular(16), - ), + Widget _buildProgressIndicator(bool isDarkMode) { + return Obx(() { + final progress = controller.progress.value; + final type = controller.type.value; + + // // 处理升级失败状态:弹出 snackbar 并显示错误提示 + // if (type == OtaUpgradeController.UPGRADE_FAILED || + // type == OtaUpgradeController.CONNECTION_DISCONNECTION) { + // // 使用延迟确保在 build 完成后执行 + // Future.delayed(Duration.zero, () { + // Get.snackbar('升级失败', '重新连接设备后重试'); + // }); + + // return Container( + // width: double.infinity, + // padding: const EdgeInsets.symmetric(vertical: 10), + // child: Text( + // '升级失败,请重新连接设备', + // style: TextStyle( + // fontSize: 18, + // fontWeight: FontWeight.bold, + // color: Colors.red[700], + // ), + // ), + // ); + // } + + // 正常升级状态 + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 10), child: Column( children: [ - Text('正在下载更新...'.tr, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.blue[300] : Colors.blue[700])), - SizedBox(height: 16), - //SizedBox( - //width: 240, - Container( - decoration: BoxDecoration( - border: Border.all( - color: isDarkMode ? Colors.blue[400]! : Colors.blue[300]!, - width: 1.5, - ), - borderRadius: BorderRadius.circular(12), + Text( + controller.getStatusMessage(type), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.blue[300] : Colors.blue[700], ), - child: LinearProgressIndicator( - value: controller.downloadProgress.value, - backgroundColor: - isDarkMode ? Colors.grey[800] : Colors.grey[200], - valueColor: AlwaysStoppedAnimation( - isDarkMode ? Colors.blue[300] : Colors.blue), - minHeight: 12, - borderRadius: BorderRadius.circular(10), + ), + const SizedBox(height: 16), + LinearProgressIndicator( + value: progress / 100.0, // 转换为 0.0-1.0 + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200], + valueColor: AlwaysStoppedAnimation( + isDarkMode ? Colors.blue[300]! : Colors.blue, + ), + ), + const SizedBox(height: 8), + Text( + '${progress.toStringAsFixed(1)}%', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.blue[300] : Colors.blue[700], ), ), - SizedBox(height: 16), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.downloading, - color: isDarkMode ? Colors.blue[300] : Colors.blue, - size: 20, - ), - SizedBox(width: 8), - Text( - '${(controller.downloadProgress.value * 100).toStringAsFixed(1)}%', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: - isDarkMode ? Colors.blue[300] : Colors.blue[700])), - ], - ) ], ), ); + }); + } } diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 984376632..aa8ac4d54 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -8,6 +8,7 @@ import '../../../data/services/speech_factory.dart'; import '../models/language_model.dart'; import 'package:logger/logger.dart'; import '../../../data/models/user_Info.dart'; +import '../../../data/services/ble_manager.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; @@ -402,4 +403,42 @@ class SettingsController extends GetxController { sourceLanguage.value = targetLanguage.value; targetLanguage.value = temp; } + + /// 清除BLE配对设备 + Future clearBleDevices() async { + // 显示确认对话框 + final confirmed = await Get.dialog( + AlertDialog( + title: const Text('清除配对设备'), + content: const Text('确定要清除所有BLE配对设备吗?此操作不可撤销。'), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: Text('cancel'.tr), + ), + TextButton( + onPressed: () => Get.back(result: true), + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('确定'), + ), + ], + ), + ); + + if (confirmed == true) { + try { + final bleManager = Get.find(); + final result = await bleManager.clearPairedDevices(); + if (result) { + Get.snackbar('成功', 'BLE配对设备已清除'); + } else { + Get.snackbar('失败', '清除BLE配对设备失败'); + } + } catch (e) { + Get.snackbar('错误', '清除BLE配对设备时发生错误'); + } + } + } } diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index ce889eb10..9fe16c6dc 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -481,6 +481,30 @@ class SettingsView extends GetView { color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]), + + // 清除配对设备 + _buildSimpleNavigationSetting( + title: '清除配对设备', + subtitle: '清除所有BLE配对设备记录', + icon: Icons.bluetooth_disabled, + iconBgColor: isDarkMode + ? Colors.red[900]!.withOpacity(0.3) + : Colors.red[100]!, + iconColor: + isDarkMode ? Colors.red[300]! : Colors.red[600]!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + onTap: () { + controller.clearBleDevices(); + }, + isDarkMode: isDarkMode, + ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 杰理测试 _buildSimpleNavigationSetting( title: '杰理蓝牙测试', diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index 64a0b9518..1fd600fb7 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/local_plugins/agent_service/android/build.gradle.kts @@ -48,5 +48,5 @@ dependencies { add("compileOnly", project(":music_service")) add("compileOnly", project(":amap_service")) add("compileOnly", project(":speech")) - add("compileOnly", project(":open_ai_service")) + add("compileOnly", project(":chat_api")) } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/AndroidManifest.xml b/local_plugins/agent_service/android/src/main/AndroidManifest.xml index ac04b7dca..b9e7e226b 100644 --- a/local_plugins/agent_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/agent_service/android/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ android:name=".BleMediaService" android:exported="true" android:enabled="true" - android:foregroundServiceType="mediaPlayback"> + android:foregroundServiceType="connectedDevice"> diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 94eae0890..d9f358d34 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -4,7 +4,8 @@ import android.content.Context import org.json.JSONArray import org.json.JSONObject import com.yunqiinnovation.azure_speech.AzureAsrHelper -import com.yunqiinnovation.open_ai_service.OpenAIService +import com.yunqiinnovation.chat_api.ChatApiService +import com.yunqiinnovation.chat_api.StreamCallback import com.yunqiinnovation.chat_storage.ChatStorageHelper import com.yunqiinnovation.music_service.MusicServiceStarter import com.yunqiinnovation.amap_service.NavigationServiceHelper @@ -53,8 +54,8 @@ object AgentService : CoroutineScope { // 使用AzureTtsHelper作为唯一的TTS实现 private var ttsService: AzureTtsHelper? = null - // OpenAI服务 - 修改为使用OpenAIService - private lateinit var openAIService: OpenAIService + // ChatAPI服务 - 使用新的ChatApiService + private lateinit var chatApiService: ChatApiService // 聊天存储服务 private lateinit var chatStorageHelper: ChatStorageHelper @@ -121,7 +122,7 @@ object AgentService : CoroutineScope { try { // 初始化上下文和服务 this.context = appContext.applicationContext - this.openAIService = OpenAIService(context) + this.chatApiService = ChatApiService(context) this.chatStorageHelper = ChatStorageHelper(context) this.audioPlayer = AudioPlayer(context) @@ -146,8 +147,8 @@ object AgentService : CoroutineScope { // 初始化TTS服务 initializeTtsEngine(config) - // 初始化OpenAI服务 - openAIService.initialize( + // 初始化ChatAPI服务 + chatApiService.initialize( config["openaiApiKey"]?.toString() ?: "", config["openaiBaseUrl"]?.toString() ?: "", config["openaiModel"]?.toString() ?: "", @@ -181,9 +182,9 @@ object AgentService : CoroutineScope { azureAsrHelper?.dispose() - // 释放OpenAI服务 - if (::openAIService.isInitialized) { - openAIService.cancelAll() + // 释放ChatAPI服务 + if (::chatApiService.isInitialized) { + chatApiService.cancelCurrentStream() } // 释放TTS服务 @@ -401,7 +402,7 @@ object AgentService : CoroutineScope { "language" to detectedLanguage )) - processWithOpenAI(result) + processWithChatApi(result) } // 重置状态,继续识别 @@ -513,8 +514,8 @@ object AgentService : CoroutineScope { currentAiJob?.cancel() currentAiJob = null - // 通知OpenAI服务终止当前流式请求 - openAIService.cancelCurrentStream() + // 通知ChatAPI服务终止当前流式请求 + chatApiService.cancelCurrentStream() // 记录日志 Log.d(TAG, "AI流输出已停止") @@ -543,43 +544,43 @@ object AgentService : CoroutineScope { return false } - // 使用OpenAI处理文本 - processWithOpenAI(text, speakResponse) + // 使用ChatAPI处理文本 + processWithChatApi(text, speakResponse) return true } /** - * 使用OpenAI处理语音识别结果 + * 使用ChatAPI处理语音识别结果 */ - private fun processWithOpenAI(text: String) { - processWithOpenAI(text, true) + private fun processWithChatApi(text: String) { + processWithChatApi(text, true) } /** - * 使用OpenAI处理文本消息 + * 使用ChatAPI处理文本消息 * * @param text 用户输入文本 * @param speakResponse 是否使用TTS朗读回复 */ - private fun processWithOpenAI( + private fun processWithChatApi( text: String, speakResponse: Boolean = true ) { Log.d(TAG, "用户问题: $text") // 创建用户文本消息并处理 - val userMessage = openAIService.createUserMessage(text) - processWithOpenAIInternal(userMessage, text, speakResponse) + val userMessage = createUserMessage(text) + processWithChatApiInternal(userMessage, text, speakResponse) } /** - * 使用OpenAI处理图片 + * 使用ChatAPI处理图片 * * @param imageBase64 Base64编码的图片数据 * @param text 可选的文本描述或问题 * @param speakResponse 是否朗读回复 */ - private fun processImageWithOpenAI( + private fun processImageWithChatApi( imageBase64: String, text: String = "", speakResponse: Boolean = false @@ -587,14 +588,14 @@ object AgentService : CoroutineScope { Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}") // 创建带图片的用户消息并处理 - val userMessage = openAIService.createUserMessageWithImage(text, imageBase64) + val userMessage = createUserMessageWithImage(text, imageBase64) // 图片描述用于存储 val displayText = text.ifEmpty { "[图片]" } - processWithOpenAIInternal(userMessage, displayText, speakResponse, true) + processWithChatApiInternal(userMessage, displayText, speakResponse, true) } /** - * 内部方法:通用的OpenAI处理逻辑 + * 内部方法:通用的ChatAPI处理逻辑 * * @param userMessage 用户消息(可以是文本或图片格式) * @param displayText 用于显示和存储的文本 @@ -602,7 +603,7 @@ object AgentService : CoroutineScope { * @param hasImage 是否包含图片 * @param imagePath 图片路径(如果有) */ - private fun processWithOpenAIInternal( + private fun processWithChatApiInternal( userMessage: JSONObject, displayText: String, speakResponse: Boolean = true, @@ -634,7 +635,7 @@ object AgentService : CoroutineScope { // 先添加系统提示词 if (systemPrompt.isNotEmpty()) { - messagesWithSystemPrompt.put(openAIService.createSystemMessage(systemPrompt)) + messagesWithSystemPrompt.put(createSystemMessage(systemPrompt)) // Log.d(TAG, "系统提示词: ${systemPrompt}") } @@ -672,9 +673,9 @@ object AgentService : CoroutineScope { addToHistoryMessages(userMessage) } - openAIService.sendMessageStream( + chatApiService.sendMessageStream( messagesWithSystemPrompt, - object : OpenAIService.StreamCallback { + object : StreamCallback { override fun onToken(token: String) { responseBuilder.append(token) if (speakResponse && !nobroadcast) { @@ -704,7 +705,7 @@ object AgentService : CoroutineScope { sendEvent("assistant_response", responseData) // 添加AI回复到历史记录 - addToHistoryMessages(openAIService.createAssistantMessage(response)) + addToHistoryMessages(createAssistantMessage(response)) // 保存聊天记录 saveChatMessage(displayText, response,aiMetadata,userMetadata.toString()) @@ -796,9 +797,9 @@ object AgentService : CoroutineScope { val content = message["message"] as String if (sender == "user") { - addToHistoryMessages(openAIService.createUserMessage(content)) + addToHistoryMessages(createUserMessage(content)) } else if (sender == "assistant") { - addToHistoryMessages(openAIService.createAssistantMessage(content)) + addToHistoryMessages(createAssistantMessage(content)) } } @@ -945,6 +946,62 @@ object AgentService : CoroutineScope { return result == android.content.pm.PackageManager.PERMISSION_GRANTED } + /** + * 创建用户消息 + */ + private fun createUserMessage(content: String): JSONObject { + return JSONObject().apply { + put("role", "user") + put("content", content) + } + } + + /** + * 创建系统消息 + */ + private fun createSystemMessage(content: String): JSONObject { + return JSONObject().apply { + put("role", "system") + put("content", content) + } + } + + /** + * 创建助手消息 + */ + private fun createAssistantMessage(content: String): JSONObject { + return JSONObject().apply { + put("role", "assistant") + put("content", content) + } + } + + /** + * 创建带图片的用户消息 + */ + private fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject { + val content = JSONArray().apply { + if (text.isNotEmpty()) { + put(JSONObject().apply { + put("type", "text") + put("text", text) + }) + } + put(JSONObject().apply { + put("type", "image_url") + put("image_url", JSONObject().apply { + put("url", "data:image/jpeg;base64,$imageBase64") + put("detail", "auto") + }) + }) + } + + return JSONObject().apply { + put("role", "user") + put("content", content) + } + } + /** * 处理图片输入 */ @@ -971,7 +1028,7 @@ object AgentService : CoroutineScope { launch { try { // 将图片转换为Base64格式 - val imageBase64 = openAIService.fileToBase64(imagePath) ?: run { + val imageBase64 = chatApiService.fileToBase64(imagePath) ?: run { sendEvent("error", mapOf( "code" to "IMAGE_CONVERSION_FAILED", "message" to "图片转换失败" @@ -980,9 +1037,9 @@ object AgentService : CoroutineScope { } // 创建带图片的用户消息并处理 - val userMessage = openAIService.createUserMessageWithImage(text, imageBase64) - // 图片描述用于存储 - val displayText = text.ifEmpty { "[图片]" } + val userMessage = createUserMessageWithImage(text, imageBase64) + // 图片描述用于存储 + val displayText = text.ifEmpty { "[图片]" } // 通知图片准备完成 sendEvent("image_ready", mapOf( @@ -991,7 +1048,7 @@ object AgentService : CoroutineScope { )) // 处理消息并传入存储路径 - processWithOpenAIInternal(userMessage, displayText, speakResponse, true, imagePath) + processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath) } catch (e: Exception) { Log.e(TAG, "处理图片失败: ${e.message}") sendEvent("error", mapOf( diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index 0a811f33a..e7452eb45 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -5,46 +5,35 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service -import android.bluetooth.BluetoothManager -import android.bluetooth.le.ScanFilter -import android.bluetooth.le.ScanSettings -import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.SharedPreferences -import android.media.AudioManager -import android.media.MediaPlayer import android.os.Build import android.os.IBinder -import android.support.v4.media.session.MediaSessionCompat -import android.support.v4.media.session.PlaybackStateCompat import androidx.core.app.NotificationCompat -import androidx.media.session.MediaButtonReceiver -import com.yunqiinnovation.agent_service.R import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean -import android.view.KeyEvent import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import com.yunqiinnovation.ble_service.BleConst import com.yunqiinnovation.ble_service.BleService + /** - * 语音助手媒体服务 + * BLE代理服务 * 职责: - * 1. 接收并处理媒体按钮事件(播放/暂停等) - * 2. 维持媒体会话,使系统可以通过媒体按钮唤醒应用 - * 3. 最小化实现,不直接对接Flutter层 + * 1. 语音代理初始化和管理 + * 2. BLE设备连接管理 + * 3. 后台扫描结果处理 */ class BleMediaService : Service(), CoroutineScope { companion object { - private const val TAG = "BleMediaService" - private const val CHANNEL_ID = "ble_media_service_channel" + private const val TAG = "BleAgentService" + private const val CHANNEL_ID = "ble_agent_service_channel" private const val NOTIFICATION_ID = 1002 private const val PREF_NAME = "deep_voice_config" @@ -70,18 +59,10 @@ class BleMediaService : Service(), CoroutineScope { private val job = SupervisorJob() override val coroutineContext: CoroutineContext get() = Dispatchers.IO + job - - private lateinit var mediaSession: MediaSessionCompat - private lateinit var audioManager: AudioManager - - // 媒体操作防抖 - private var lastActionTime = 0L + // BLE扫描防抖 private var lastScanTime = 0L - private val ACTION_DEBOUNCE_MS = 800L // 媒体操作防抖时间(毫秒) - - // 添加音频播放器 - private var audioPlayer: MiniMediaPlayer? = null + private val SCAN_DEBOUNCE_MS = 800L // BLE扫描防抖时间(毫秒) // 语音代理是否已初始化 private var isChatAgentInitialized = false @@ -91,106 +72,85 @@ class BleMediaService : Service(), CoroutineScope { override fun onCreate() { super.onCreate() - Log.i(TAG, "BleMediaService正在创建...") + Log.i(TAG, "BLE代理服务正在创建...") // 设置服务状态 isRunning.set(true) startForeground(NOTIFICATION_ID, createNotification()) - // 初始化音频管理器 - audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - - // 创建并启动媒体会话 - setupMediaSession() - // 注册媒体按钮接收器 - // registerMediaButtonReceiver() - // // 注册后台扫描 - // registerBackgroundScan() - - // 创建音频播放器,并播放静音文件 - audioPlayer = MiniMediaPlayer(this) - audioPlayer?.play(R.raw.silence, 0f) - setPlaybackState(PlaybackStateCompat.STATE_PAUSED) - - Log.i(TAG, "媒体服务已启动") + Log.i(TAG, "BLE代理服务已启动") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}") + Log.i(TAG, "BleAgentService.onStartCommand: ${intent?.action}") // 保存当前Intent,用于后续初始化 currentIntent = intent - // 初始化语音代理 + + // 初始化语音代理 if (!isChatAgentInitialized) { initializeChatAgent() } - // 如果是媒体按钮事件,交给MediaButtonReceiver处理 - if (Intent.ACTION_MEDIA_BUTTON == intent?.action) { - MediaButtonReceiver.handleIntent(mediaSession, intent) - return START_STICKY - } // 处理蓝牙扫描结果事件 if (intent?.action == BleConst.ACTION_SCAN_RESULT) { - val currentTime = System.currentTimeMillis() - if (currentTime - lastScanTime >= ACTION_DEBOUNCE_MS) { - lastScanTime = currentTime - checkBleConnection() - } - + val currentTime = System.currentTimeMillis() + if (currentTime - lastScanTime >= SCAN_DEBOUNCE_MS) { + lastScanTime = currentTime + checkBleConnection() + } return START_STICKY } - - return START_STICKY } - /** * 初始化语音交互代理 */ private fun initializeChatAgent() { - try { - // 获取SharedPreferences - val prefs = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - - // 尝试从Intent中获取参数 - val configFromIntent = extractConfigFromIntent(currentIntent) - - // 如果Intent中有参数,则保存到SharedPreferences - if (configFromIntent.isNotEmpty()) { - Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") - saveConfigToPreferences(configFromIntent, prefs) - } - - // 合并Intent和SharedPreferences中的参数 - val config = getConfigFromPreferences(prefs) - Log.d(TAG, "当前配置参数: $config") - // 检查必要的配置是否存在 - if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) { - Log.w(TAG, "语音代理初始化失败: API密钥未配置") - return - } - - if (isChatAgentInitialized) { - Log.d(TAG, "ChatAgent已经初始化,跳过") - return - } - if(config.isNotEmpty()) { - // 初始化ChatAgent - Log.i(TAG, "开始初始化ChatAgent") - val success = BleAgent.initialize(applicationContext, config) - if (success) { - isChatAgentInitialized = true - Log.i(TAG, "语音交互代理初始化成功") - } else { - Log.e(TAG, "语音交互代理初始化失败") - } + try { + // 获取SharedPreferences + val prefs = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + + // 尝试从Intent中获取参数 + val configFromIntent = extractConfigFromIntent(currentIntent) + + // 如果Intent中有参数,则保存到SharedPreferences + if (configFromIntent.isNotEmpty()) { + Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") + saveConfigToPreferences(configFromIntent, prefs) + } + + // 合并Intent和SharedPreferences中的参数 + val config = getConfigFromPreferences(prefs) + Log.d(TAG, "当前配置参数: $config") + + // 检查必要的配置是否存在 + if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) { + Log.w(TAG, "语音代理初始化失败: API密钥未配置") + return + } + + if (isChatAgentInitialized) { + Log.d(TAG, "ChatAgent已经初始化,跳过") + return + } + + if(config.isNotEmpty()) { + // 初始化ChatAgent + Log.i(TAG, "开始初始化ChatAgent") + val success = BleAgent.initialize(applicationContext, config) + if (success) { + isChatAgentInitialized = true + Log.i(TAG, "语音交互代理初始化成功") + } else { + Log.e(TAG, "语音交互代理初始化失败") } - } catch (e: Exception) { - Log.e(TAG, "初始化语音交互代理时发生异常", e) } + } catch (e: Exception) { + Log.e(TAG, "初始化语音交互代理时发生异常", e) + } } /** @@ -255,23 +215,7 @@ class BleMediaService : Service(), CoroutineScope { override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() { - Log.i(TAG, "BleMediaService正在销毁...") - - // 取消注册媒体按钮接收器 - try { - val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) - audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver) - Log.i(TAG, "媒体按钮接收器已取消注册") - } catch (e: Exception) { - Log.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e) - } - - // 释放媒体会话 - mediaSession.release() - - // 停止并释放音频播放器 - audioPlayer?.release() - audioPlayer = null + Log.i(TAG, "BLE代理服务正在销毁...") // 取消所有协程 cancel() @@ -281,62 +225,12 @@ class BleMediaService : Service(), CoroutineScope { super.onDestroy() } - - /** - * 设置媒体会话,处理媒体按钮事件 - */ - private fun setupMediaSession() { - mediaSession = MediaSessionCompat(this, "BleMediaSession").apply { - setFlags( - MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or - MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS - ) - // 设置回调,处理媒体按钮事件 - setCallback(object : MediaSessionCompat.Callback() { - - override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean { - - // 使用系统默认的媒体按钮处理 - return super.onMediaButtonEvent(mediaButtonIntent) - } - }) - - // 设置初始播放状态 - val stateBuilder = PlaybackStateCompat.Builder() - .setActions( - PlaybackStateCompat.ACTION_PLAY or - PlaybackStateCompat.ACTION_PAUSE or - PlaybackStateCompat.ACTION_SKIP_TO_NEXT or - PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - ) - .setState(PlaybackStateCompat.STATE_STOPPED, 0, 1.0f) - setPlaybackState(stateBuilder.build()) - - // 激活会话,使其可以接收媒体按钮事件 - isActive = true - } - } - - /** - * 注册媒体按钮接收器 - */ - private fun registerMediaButtonReceiver() { - try { - val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) - audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver) - Log.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver") - } catch (e: Exception) { - Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) - } - } - private fun checkBleConnection() { - Log.i(TAG, "checkBleConnection收到连接请求") try { - // 示例:使用协程异步处理连接 + // 使用协程异步处理连接 launch { if(!BleService.isConnected()) { BleService.connect() @@ -347,44 +241,6 @@ class BleMediaService : Service(), CoroutineScope { } } - /** - * 更新通知内容 - */ - private fun updateNotification() { - try { - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(NOTIFICATION_ID, createNotification()) - } catch (e: Exception) { - Log.e(TAG, "更新通知异常: ${e.message}") - } - } - - /** - * 设置播放状态 - */ - private fun setPlaybackState(state: Int) { - val playbackStateBuilder = PlaybackStateCompat.Builder() - .setActions( - PlaybackStateCompat.ACTION_PLAY or - PlaybackStateCompat.ACTION_PAUSE or - PlaybackStateCompat.ACTION_PLAY_PAUSE or - PlaybackStateCompat.ACTION_SKIP_TO_NEXT or - PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or - PlaybackStateCompat.ACTION_STOP or - PlaybackStateCompat.ACTION_SEEK_TO or - PlaybackStateCompat.ACTION_FAST_FORWARD or - PlaybackStateCompat.ACTION_REWIND - ) - // 使用更高的播放速度,让系统认为是真正的播放 - .setState(state, 0, 1.0f, System.currentTimeMillis()) - mediaSession.setPlaybackState(playbackStateBuilder.build()) - - // 确保MediaSession激活 - if (!mediaSession.isActive) { - mediaSession.isActive = true - } - } - /** * 创建前台服务通知 */ @@ -395,7 +251,7 @@ class BleMediaService : Service(), CoroutineScope { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, - "语音助手媒体服务", + "语音助手代理服务", NotificationManager.IMPORTANCE_LOW ) manager.createNotificationChannel(channel) @@ -419,77 +275,12 @@ class BleMediaService : Service(), CoroutineScope { // 构建通知 val builder = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("DeepSound") - // .setContentText(if (isPlaying) "正在播放" else "已暂停") - .setSmallIcon(android.R.drawable.ic_media_play) + .setContentText("语音助手代理服务运行中") + .setSmallIcon(android.R.drawable.ic_dialog_info) // 设置点击意图 pendingIntent?.let { builder.setContentIntent(it) } return builder.build() } - - /** - * 简易媒体播放器,用于播放静音资源 - */ - inner class MiniMediaPlayer(private val context: Context) { - private var mediaPlayer: MediaPlayer? = null - - /** - * 播放资源文件 - * @param resId 资源ID - * @param volume 音量 (0.0-1.0) - */ - fun play(resId: Int, volume: Float = 0f) { - try { - // 释放之前的资源 - release() - - // 创建播放器并设置资源 - mediaPlayer = MediaPlayer().apply { - // 设置资源 - context.resources.openRawResourceFd(resId)?.use { fd -> - setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) - } - - // 设置音量 - setVolume(volume, volume) - - // // 播放完成后自动循环播放,保持媒体会话活跃 - // setOnCompletionListener { - // Log.i(TAG, "静音资源播放完成,再次播放保持会话活跃") - // play(resId, volume) - // } - - // 准备并播放 - prepare() - start() - } - - Log.i(TAG, "开始播放静音资源") - } catch (e: Exception) { - Log.e(TAG, "播放静音资源异常: ${e.message}", e) - release() - } - } - - /** - * 释放资源 - */ - fun release() { - try { - mediaPlayer?.apply { - if (isPlaying) stop() - release() - } - mediaPlayer = null - } catch (e: Exception) { - // 忽略异常 - } - } - - /** - * 是否正在播放 - */ - fun isPlaying() = mediaPlayer?.isPlaying == true - } } \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index b3810ea94..c000e1faa 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -30,7 +30,6 @@ import com.jieli.jl_audio_decode.callback.OnStateCallback import com.jieli.jl_audio_decode.opus.OpusManager import com.jieli.jl_audio_decode.exceptions.OpusException import com.yunqiinnovation.ble_service.BleCompanionManager -import com.yunqiinnovation.ble_service.OTAManager import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure; import com.jieli.jl_bt_ota.constant.ErrorCode; import com.jieli.jl_bt_ota.constant.StateCode; @@ -89,7 +88,8 @@ object BleService { private lateinit var scanHandler: Handler private val scanResults = ArrayList() private var isScanning = false - +// 是否是ota模式 + var isEnterOta = false // 连接状态 val connectionState = MutableLiveData(BleConst.STATE_DISCONNECTED) @@ -354,10 +354,14 @@ object BleService { return } - Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接") - + + if(!isEnterOta) + { + Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接") // 调用现有的 connect 方法 connect(pairedMac) + } + } catch (e: Exception) { Log.e(TAG, "自动连接已配对设备失败: ${e.message}", e) updateConnectionState(BleConst.STATE_ERROR) @@ -428,44 +432,6 @@ object BleService { } } - // fun startOTA() { - // try { - // otaManager?.startOTA(CustomUpdateCallback(context, null)) - // Log.i(TAG, "OTA 升级已启动") - // } catch (e: IllegalStateException) { - // Log.e(TAG, "OTA 启动失败: ${e.message}") - // disconnectGatt() - // } - // } - - // // 配置OTA - // fun configureOTA() { - // try { - // val firmwarePath = - // context.getExternalFilesDir("downloads/ota_upgrades")?.path + "/1.ufw" - // val file = File(firmwarePath) - // if (!file.exists()) { - // // throw IOException("Firmware file not found") - // Log.i(TAG, "Firmware file not found") - // } - - // val bluetoothOption = BluetoothOTAConfigure().apply { - // priority = BluetoothOTAConfigure.PREFER_BLE - // isUseReconnect = true - // isUseAuthDevice = true - // mtu = 512 - // isNeedChangeMtu = false // 确保MTU请求生效 - // isUseJLServer = false - // firmwareFilePath = firmwarePath - // } - // otaManager?.configure(bluetoothOption) ?: run { - // Log.e(TAG, "OTA Manager未初始化") - // } - // } catch (e: Exception) { - // Log.e(TAG, "OTA配置失败: ${e.message}") - // // 处理错误 - // } - // } /** * 更新连接状态并通知回调 diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt deleted file mode 100644 index b28149983..000000000 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.yunqiinnovation.ble_service -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGatt -import android.bluetooth.BluetoothProfile -import android.content.Context -import com.jieli.jl_bt_ota.constant.ErrorCode; -import com.jieli.jl_bt_ota.constant.StateCode; -import com.jieli.jl_bt_ota.impl.BluetoothOTAManager; -import com.jieli.jl_bt_ota.interfaces.BtEventCallback; -import com.jieli.jl_bt_ota.interfaces.IActionCallback; -import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback; -import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure; -import com.jieli.jl_bt_ota.model.base.BaseError; -import com.jieli.jl_bt_ota.model.response.TargetInfoResponse; -import android.util.Log -import java.util.* - class CustomUpdateCallback( - val context: Context, - val device: BluetoothDevice? -) : IUpgradeCallback { - companion object { - private const val TAG = "JlOtaPlugin" - private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" - private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" - - } - - override fun onStartOTA() { - Log.d(TAG, "onStartOTA开始升级") - // viewModel.otaStateMLD.value = OTAStart(device) - } - - override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) { - Log.d(TAG, "onNeedReconnect需要重新链接") - - } - - override fun onProgress(type: Int, progress: Float) { - Log.d(TAG, "onProgress正在进展") - //viewModel.otaStateMLD.value = OTAWorking(device, type, progress) - } - - override fun onStopOTA() { - Log.d(TAG, "onStartOTA停止升级") - - } - - override fun onCancelOTA() { - Log.d(TAG, "onCancelOTA取消升级") - - } - - override fun onError(error: BaseError?) { - val errorMessage = error?.message ?: "Unknown error" - Log.e(TAG, "升级失败: ${error?.message ?: "未知错误"}") - - // 如果需要打印完整错误对象信息(假设BaseError有重写toString()) - Log.d(TAG, "完整错误对象: ${error ?: "null"}") - } -} - -class OTAManager(context: Context) : BluetoothOTAManager(context) { - - - // 定义回调接口 - interface DataCallback { - fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) - } - // 保存回调的实例 - private var dataCallback: DataCallback? = null - - // 设置回调的方法 - fun setDataCallback(callback: DataCallback) { - this.dataCallback = callback - } - override fun getConnectedDevice(): BluetoothDevice? { - // 这里可以添加空实现的注释说明 - Log.i(TAG, "ota数据发送getConnectedDevice") - return null - } - - override fun getConnectedBluetoothGatt(): BluetoothGatt? { - // 这里可以添加空实现的注释说明 - Log.i(TAG, "ota数据发送getConnectedBluetoothGatt") - return null - } - - override fun connectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { - // 这里可以添加空实现的注释说明 - Log.i(TAG, "ota数据发送connectBluetoothDevice") - } - - override fun disconnectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { - // 这里可以添加空实现的注释说明 - Log.i(TAG, "ota数据发送disconnectBluetoothDevice") - } - - /** 数据发送方法(协议无关) */ - override fun sendDataToDevice( - bluetoothDevice: BluetoothDevice?, - bytes: ByteArray? - ): Boolean { - Log.i(TAG, "ota数据发送${bluetoothDevice},${bytes?.size}") - // 触发回调,将数据回传给调用者 - dataCallback?.onDataReceived(bluetoothDevice, bytes) - // context.onReceiveDeviceData(device, data); - return true - } -} - diff --git a/local_plugins/chat_api/README.md b/local_plugins/chat_api/README.md index be47e00d0..463e1c3d6 100644 --- a/local_plugins/chat_api/README.md +++ b/local_plugins/chat_api/README.md @@ -15,6 +15,7 @@ ## 平台支持 - iOS 16.0+ +- Android (API 21+) ## MCP 功能 @@ -138,6 +139,7 @@ chatApi.eventStream.listen((event) { ## 架构 +### iOS ``` ChatApiService (Flutter层) ↓ @@ -150,3 +152,14 @@ ChatApiService (iOS原生服务) └── SystemFunctionHandler (内置系统功能) ``` +### Android +``` +ChatApiService (Flutter层) + ↓ +ChatApiPlugin (Android插件桥接) + ↓ +ChatApiService (Android原生服务) + ├── OpenAI客户端 (aallam/openai-kotlin 4.0.1) + └── MCP功能 (暂时留空,接口预留) +``` + diff --git a/local_plugins/chat_api/android/README.md b/local_plugins/chat_api/android/README.md new file mode 100644 index 000000000..99c528b5f --- /dev/null +++ b/local_plugins/chat_api/android/README.md @@ -0,0 +1,70 @@ +# ChatAPI Android 实现 + +基于 [openai-kotlin 4.0.1](https://github.com/aallam/openai-kotlin/tree/4.0.1) 的 ChatAPI 插件 Android 实现。 + +## 特性 + +- ✅ 基于 openai-kotlin 4.0.1 的 OpenAI API 客户端 +- ✅ 与 iOS 版本接口完全一致 +- ✅ 支持流式和非流式对话 +- ✅ 支持多模态内容(文本和图像识别) +- ✅ 工具调用架构预留(MCP 功能暂时留空) +- ✅ 协程支持和异步处理 +- ✅ 完整的错误处理和取消机制 + +## 依赖 + +- openai-kotlin 4.0.1 +- ktor-client-okhttp 2.3.2 +- kotlinx-coroutines +- gson + +## 主要文件 + +- `ChatApiPlugin.kt` - Flutter 插件桥接层 +- `ChatApiService.kt` - 核心 ChatAPI 服务实现 +- `StreamCallback.kt` - 流式回调接口 + +## 与 iOS 版本的一致性 + +Android 实现完全遵循 iOS 版本的接口定义: + +1. **初始化方法**: `initialize(apiKey, baseUrl, model, mcpServer)` +2. **消息创建**: `createUserMessage()`, `createAssistantMessage()` +3. **消息发送**: `sendMessage()` (非流式), `sendMessageStream()` (流式) +4. **函数注册**: `registerFunction()` +5. **MCP 相关**: 接口保留,功能暂时留空 +6. **流式事件**: 完全一致的事件类型和回调 + +## MCP 功能状态 + +MCP (Model Context Protocol) 相关功能在 Android 版本中暂时留空,包括: + +- `initializeMcpClient()` - 返回 false +- `isMcpInitialized()` - 返回 false +- `handleMcpToolCall()` - 返回占位符消息 +- 工具调用会通知上层但不会实际执行 + +## 使用示例 + +```kotlin +// 初始化 +chatApiService.initialize( + apiKey = "your-api-key", + baseUrl = "https://api.openai.com/v1/", + model = "gpt-3.5-turbo", + mcpServer = "" +) + +// 流式对话 +chatApiService.sendMessageStream(listOf( + mapOf("role" to "user", "content" to "Hello!") +)) +``` + +## 注意事项 + +1. 需要网络权限 `INTERNET` 和 `ACCESS_NETWORK_STATE` +2. 所有网络请求在后台线程执行 +3. 流式回调在主线程触发 +4. 支持取消正在进行的请求 \ No newline at end of file diff --git a/local_plugins/chat_api/android/build.gradle.kts b/local_plugins/chat_api/android/build.gradle.kts new file mode 100644 index 000000000..1eb929ecf --- /dev/null +++ b/local_plugins/chat_api/android/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + kotlin("plugin.serialization") version "1.9.24" +} + +android { + namespace = "com.yunqiinnovation.chat_api" + compileSdk = 35 + + defaultConfig { + minSdk = 21 + targetSdk = 33 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } + + packaging { + resources { + excludes.add("META-INF/DEPENDENCIES") + excludes.add("META-INF/LICENSE") + excludes.add("META-INF/LICENSE.txt") + excludes.add("META-INF/license.txt") + excludes.add("META-INF/NOTICE") + excludes.add("META-INF/NOTICE.txt") + excludes.add("META-INF/notice.txt") + excludes.add("META-INF/*.kotlin_module") + } + } +} + +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + + // OpenAI Kotlin 4.0.1 + implementation("com.aallam.openai:openai-client:4.0.1") + implementation("io.ktor:ktor-client-okhttp:2.3.2") + + // JSON处理 + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + implementation("com.google.code.gson:gson:2.10.1") +} \ No newline at end of file diff --git a/local_plugins/chat_api/android/settings.gradle.kts b/local_plugins/chat_api/android/settings.gradle.kts new file mode 100644 index 000000000..c953e09ff --- /dev/null +++ b/local_plugins/chat_api/android/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "chat_api" \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/AndroidManifest.xml b/local_plugins/chat_api/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..11db23350 --- /dev/null +++ b/local_plugins/chat_api/android/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt new file mode 100644 index 000000000..bc7801951 --- /dev/null +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt @@ -0,0 +1,252 @@ +package com.yunqiinnovation.chat_api + +import android.os.Handler +import android.os.Looper +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import kotlinx.coroutines.* + +/** + * ChatApiPlugin + * + * 基于 openai-kotlin 4.0.1 的 ChatAPI 插件 Android 实现 + * 与 iOS 版本接口完全一致 + */ +class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler { + private lateinit var methodChannel: MethodChannel + private lateinit var eventChannel: EventChannel + private var eventSink: EventChannel.EventSink? = null + + private val chatApiService = ChatApiService() + private val mainHandler = Handler(Looper.getMainLooper()) + + // 协程作用域 + private val pluginScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/methods") + methodChannel.setMethodCallHandler(this) + + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/events") + eventChannel.setStreamHandler(this) + + // 设置流式回调 + chatApiService.setStreamCallback(object : com.yunqiinnovation.chat_api.StreamCallback { + override fun onToken(token: String) { + sendEvent("token", token) + } + + override fun onComplete() { + sendEvent("complete", null) + } + + override fun onError(error: Exception) { + sendEvent("error", error.message ?: "未知错误") + } + + override fun onFunctionCall(functionCall: org.json.JSONObject) { + try { + val jsonString = functionCall.toString() + sendEvent("functionCall", jsonString) + } catch (e: Exception) { + sendEvent("error", "Failed to serialize function call: ${e.message}") + } + } + + override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) { + try { + val meta = org.json.JSONObject().apply { + put("functionCall", functionCall) + put("functionCallResult", functionCallResult) + } + val metaString = meta.toString() + val context = functionCallResult.optString("context", "") + + sendEvent("functionCall", context, metaString) + } catch (e: Exception) { + sendEvent("error", "Failed to serialize function call result: ${e.message}") + } + } + }) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + methodChannel.setMethodCallHandler(null) + eventChannel.setStreamHandler(null) + pluginScope.cancel() + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "initialize" -> handleInitialize(call, result) + "createUserMessage" -> handleCreateUserMessage(call, result) + "createAssistantMessage" -> handleCreateAssistantMessage(call, result) + "sendMessage" -> handleSendMessage(call, result) + "sendMessageStream" -> handleSendMessageStream(call, result) + "sendFunctionCallResult" -> handleSendFunctionCallResult(call, result) + "cancelCurrentStream" -> handleCancelCurrentStream(result) + "registerFunction" -> handleRegisterFunction(call, result) + "initializeMcpClient" -> handleInitializeMcpClient(call, result) + "isMcpInitialized" -> handleIsMcpInitialized(result) + "closeMcpClient" -> handleCloseMcpClient(result) + "handleMcpToolCall" -> handleMcpToolCall(call, result) + else -> result.notImplemented() + } + } + + // MARK: - Method Handlers + + private fun handleInitialize(call: MethodCall, result: Result) { + val apiKey = call.argument("apiKey") + if (apiKey.isNullOrEmpty()) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + val baseUrl = call.argument("baseUrl") ?: "" + val model = call.argument("model") ?: "" + val mcpServer = call.argument("mcpServer") ?: "" + + val success = chatApiService.initialize(apiKey, baseUrl, model, mcpServer) + result.success(success) + } + + private fun handleCreateUserMessage(call: MethodCall, result: Result) { + val content = call.argument("content") + if (content.isNullOrEmpty()) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + val message = chatApiService.createUserMessage(content) + result.success(message) + } + + private fun handleCreateAssistantMessage(call: MethodCall, result: Result) { + val content = call.argument("content") + if (content.isNullOrEmpty()) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + val message = chatApiService.createAssistantMessage(content) + result.success(message) + } + + private fun handleSendMessage(call: MethodCall, result: Result) { + @Suppress("UNCHECKED_CAST") + val messages = call.argument>>("messages") + if (messages == null) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + pluginScope.launch { + try { + val response = chatApiService.sendMessage(messages) + mainHandler.post { + result.success(response) + } + } catch (e: Exception) { + mainHandler.post { + result.error("SEND_ERROR", e.message, null) + } + } + } + } + + private fun handleSendMessageStream(call: MethodCall, result: Result) { + @Suppress("UNCHECKED_CAST") + val messages = call.argument>>("messages") + if (messages == null) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + chatApiService.sendMessageStream(messages) + result.success(true) + } + + private fun handleSendFunctionCallResult(call: MethodCall, result: Result) { + result.error("DEPRECATED", "sendFunctionCallResult已废弃,工具调用结果现在自动处理", null) + } + + private fun handleCancelCurrentStream(result: Result) { + val success = chatApiService.cancelCurrentStream() + result.success(success) + } + + private fun handleRegisterFunction(call: MethodCall, result: Result) { + val name = call.argument("name") + val description = call.argument("description") + @Suppress("UNCHECKED_CAST") + val parameters = call.argument>("parameters") + + if (name.isNullOrEmpty() || description.isNullOrEmpty() || parameters == null) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + val success = chatApiService.registerFunction(name, description, parameters) + result.success(success) + } + + private fun handleInitializeMcpClient(call: MethodCall, result: Result) { + val serverUrl = call.argument("serverUrl") + if (serverUrl.isNullOrEmpty()) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + // MCP功能暂时留空 + result.success(false) + } + + private fun handleIsMcpInitialized(result: Result) { + // MCP功能暂时留空 + result.success(false) + } + + private fun handleCloseMcpClient(result: Result) { + // MCP功能暂时留空 + result.success(true) + } + + private fun handleMcpToolCall(call: MethodCall, result: Result) { + val functionCallJson = call.argument("functionCall") + if (functionCallJson.isNullOrEmpty()) { + result.error("INVALID_ARGS", "缺少必要参数", null) + return + } + + // MCP功能暂时留空 + result.success("MCP功能暂未实现") + } + + // MARK: - Event Stream Handler + + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + eventSink = events + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } + + private fun sendEvent(type: String, content: Any?, meta: String? = null) { + mainHandler.post { + eventSink?.let { sink -> + val event = mutableMapOf( + "type" to type + ) + content?.let { event["content"] = it } + meta?.let { event["meta"] = it } + sink.success(event) + } + } + } +} \ No newline at end of file diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt new file mode 100644 index 000000000..dcf8b01bb --- /dev/null +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -0,0 +1,799 @@ +package com.yunqiinnovation.chat_api + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Base64 +import com.aallam.openai.api.chat.* +import com.aallam.openai.api.core.Role +import com.aallam.openai.api.model.ModelId +import com.aallam.openai.api.http.Timeout +import com.aallam.openai.client.OpenAI +import com.aallam.openai.client.OpenAIConfig +import com.aallam.openai.client.OpenAIHost +import com.google.gson.Gson +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collect +import java.io.ByteArrayOutputStream +import java.io.File +import kotlin.coroutines.CoroutineContext +import kotlin.math.min +import kotlin.math.sqrt +import kotlin.time.Duration.Companion.seconds +import android.util.Log + +/** + * ChatAPI服务异常 + */ +class ChatApiException(message: String) : Exception("ChatApiException: $message") + +/** + * 流式回调接口 + * 与 iOS 版本 StreamCallback 协议保持完全一致 + */ +interface StreamCallback { + /** + * 接收到新的token + */ + fun onToken(token: String) + + /** + * 流式响应完成 + */ + fun onComplete() + + /** + * 出现错误 + */ + fun onError(error: Exception) + + /** + * 函数调用 - 兼容JSONObject格式 + */ + fun onFunctionCall(functionCall: org.json.JSONObject) + + /** + * 函数调用结果 - 兼容JSONObject格式 + */ + fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) +} + +/** + * 工具调用信息 + */ +private data class ToolCallInfo( + var id: String = "", + var name: String = "", + var arguments: String = "" +) { + fun isValid(): Boolean = id.isNotEmpty() && name.isNotEmpty() +} + +/** + * ChatAPI服务的Android实现 + * 基于 openai-kotlin 4.0.1,与 iOS 版本接口完全一致 + */ +class ChatApiService(private val context: android.content.Context? = null) : CoroutineScope { + + override val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob() + + // MARK: - 属性 + private var baseUrl = "https://api.openai.com/v1/" + private var apiKey = "" + private var model = "gpt-3.5-turbo" + private var visionModel = "gpt-4-vision-preview" + private var isInitialized = false + + // OpenAI 客户端 + private var openAI: OpenAI? = null + + // 流式请求相关 + private var currentStreamJob: Job? = null + private var streamCallback: StreamCallback? = null + private var currentMessages: List> = emptyList() + private var toolCalls: MutableMap = mutableMapOf() + private var isCanceled = false + + // JSON处理 + private val gson = Gson() + + // MARK: - 公共方法 + + /** + * 设置流式回调 + */ + fun setStreamCallback(callback: StreamCallback) { + streamCallback = callback + } + + /** + * 初始化ChatAPI服务 + */ + fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean { + this.apiKey = apiKey + if (baseUrl.isNotEmpty()) { + this.baseUrl = baseUrl + } + if (model.isNotEmpty()) { + this.model = model + } + Log.d("ChatApiService", "原始 baseUrl: $baseUrl") + + // 处理 baseUrl:移除末尾的 /chat/completions(如果存在) + // 因为 openai-kotlin 会自动拼接 /chat/completions + var processedBaseUrl = if (this.baseUrl.endsWith("/chat/completions")) { + this.baseUrl.removeSuffix("/chat/completions") + } else { + this.baseUrl.removeSuffix("/") + } + + // 确保以 / 结尾 + if (!processedBaseUrl.endsWith("/")) { + processedBaseUrl += "/" + } + + Log.d("ChatApiService", "处理后 baseUrl: $processedBaseUrl") + + return try { + // 创建OpenAI配置 + val config = OpenAIConfig( + token = apiKey, + timeout = Timeout(socket = 60.seconds), + host = OpenAIHost(baseUrl = processedBaseUrl) + ) + + openAI = OpenAI(config) + + // 初始化MCP客户端 (暂时留空,但保持接口一致) + if (mcpServer.isNotEmpty()) { + // MCP功能暂时留空,但记录服务器地址以便后续实现 + // initializeMcpClient(mcpServer) + } + + isInitialized = apiKey.isNotEmpty() + true + } catch (e: Exception) { + false + } + } + + /** + * 创建用户消息 + */ + fun createUserMessage(content: String): Map { + return mapOf( + "role" to "user", + "content" to content + ) + } + + /** + * 创建助手消息 + */ + fun createAssistantMessage(content: String): Map { + return mapOf( + "role" to "assistant", + "content" to content + ) + } + + /** + * 创建系统消息 + */ + fun createSystemMessage(content: String): Map { + return mapOf( + "role" to "system", + "content" to content + ) + } + + /** + * 发送消息(非流式输出) + */ + suspend fun sendMessage(messages: List>): String { + if (!isInitialized || apiKey.isEmpty() || openAI == null) { + throw ChatApiException("ChatAPI服务未初始化") + } + + // 检查是否包含图片,决定使用哪个模型 + val currentModel = if (containsImage(messages)) visionModel else model + + // 转换消息格式 + val chatMessages = convertToChatMessages(messages) + + // 构建请求 + val chatCompletionRequest = ChatCompletionRequest( + model = ModelId(currentModel), + messages = chatMessages, + maxTokens = 2000, + temperature = 0.7 + ) + + val result = openAI!!.chatCompletion(chatCompletionRequest) + + // 检查是否有工具调用 + val choice = result.choices.firstOrNull() + choice?.message?.toolCalls?.firstOrNull()?.let { toolCall -> + if (toolCall is ToolCall.Function) { + val functionCallJson = mapOf( + "name" to toolCall.function.name, + "arguments" to (toolCall.function.arguments ?: ""), + "id" to toolCall.id.toString() + ) + return gson.toJson(functionCallJson) + } + } + + // 返回普通消息内容 + return choice?.message?.content ?: throw ChatApiException("无效的响应格式") + } + + /** + * 发送消息(流式输出)- 兼容JSONArray格式 + */ + fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) { + // 设置回调 + setStreamCallback(callback) + + // 转换JSONArray为List> + val messageList = convertJsonArrayToList(messages) + + // 调用原始方法 + sendMessageStream(messageList) + } + + /** + * 发送消息(流式输出) + */ + fun sendMessageStream(messages: List>) { + if (!isInitialized || apiKey.isEmpty() || openAI == null) { + streamCallback?.onError(ChatApiException("ChatAPI服务未初始化")) + return + } + + // 重置状态 + isCanceled = false + currentMessages = messages + toolCalls.clear() + + // 检查是否包含图片,决定使用哪个模型 + val currentModel = if (containsImage(messages)) visionModel else model + + currentStreamJob = launch { + try { + // 转换消息格式 + val chatMessages = convertToChatMessages(messages) + + // 构建请求 + val chatCompletionRequest = ChatCompletionRequest( + model = ModelId(currentModel), + messages = chatMessages, + maxTokens = 2000, + temperature = 0.7 + ) + + val chatsFlow = openAI!!.chatCompletions(chatCompletionRequest) + + chatsFlow.collect { result -> + if (isCanceled) return@collect + + val choice = result.choices.firstOrNull() ?: return@collect + val delta = choice.delta ?: return@collect + + // 处理普通文本内容 + delta.content?.let { content -> + streamCallback?.onToken(content) + } + + // 收集工具调用信息 + delta.toolCalls?.forEach { toolCall -> + val index = toolCall.index + + // 创建或获取现有的工具调用信息 + val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() } + + // 更新ID + toolCall.id?.let { toolCallInfo.id = it.toString() } + + // 更新函数信息 + toolCall.function?.let { function -> + function.name?.let { toolCallInfo.name = it } + function.arguments?.let { toolCallInfo.arguments += it } + } + } + } + + if (!isCanceled) { + // 处理工具调用或完成 + val hasToolCalls = processToolCalls() + if (!hasToolCalls) { + streamCallback?.onComplete() + } + } + + } catch (e: Exception) { + if (!isCanceled) { + streamCallback?.onError(ChatApiException("流式请求失败: ${e.message}")) + } + } + } + } + + /** + * 处理工具调用 + */ + private suspend fun processToolCalls(): Boolean { + val firstToolCall = toolCalls.values.firstOrNull { it.isValid() } ?: return false + + // 创建函数调用字典 + val functionCall = mapOf( + "name" to firstToolCall.name, + "arguments" to firstToolCall.arguments, + "id" to firstToolCall.id + ) + + // 通知上层工具调用事件 + streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall)) + + // 在后台队列处理工具调用 + launch { + try { + if (!isCanceled) { + // 这里暂时返回占位符结果,实际MCP功能留空 + val result = mapOf("context" to "MCP功能暂未实现") + + if (!isCanceled) { + // 处理结果 + streamCallback?.onFunctionCallResult( + convertMapToJsonObject(functionCall), + convertMapToJsonObject(result) + ) + + // 将结果发送回OpenAI继续对话 + sendFunctionCallResultInternal( + messages = currentMessages, + functionCall = functionCall, + functionResult = gson.toJson(result) + ) + } + } + } catch (e: Exception) { + if (!isCanceled) { + val errorMessage = "工具调用处理失败: ${e.message}" + sendFunctionCallResultInternal( + messages = currentMessages, + functionCall = functionCall, + functionResult = errorMessage + ) + } + } + } + + return true + } + + /** + * 发送函数调用结果(内部方法) + */ + private suspend fun sendFunctionCallResultInternal( + messages: List>, + functionCall: Map, + functionResult: String + ) { + if (isCanceled) return + + val fullMessages = messages.toMutableList() + + // 添加函数调用消息 + val callId = functionCall["id"] as? String ?: "call_${System.currentTimeMillis()}" + fullMessages.add( + mapOf( + "role" to "assistant", + "content" to "", + "tool_calls" to listOf( + mapOf( + "id" to callId, + "type" to "function", + "function" to mapOf( + "name" to (functionCall["name"] as? String ?: ""), + "arguments" to (functionCall["arguments"] as? String ?: "{}") + ) + ) + ) + ) + ) + + // 添加函数结果消息 + fullMessages.add( + mapOf( + "role" to "tool", + "content" to functionResult, + "tool_call_id" to callId + ) + ) + + // 重新发起对话,让AI基于工具调用结果生成回复 + sendMessageStream(fullMessages) + } + + /** + * 取消当前流式请求 + */ + fun cancelCurrentStream(): Boolean { + isCanceled = true + currentStreamJob?.cancel() + currentStreamJob = null + return true + } + + /** + * 取消所有操作并释放资源 (与 iOS 版本保持一致) + */ + fun cancelAll() { + cancelCurrentStream() + } + + /** + * 注册函数 + */ + fun registerFunction(name: String, description: String, parameters: Map): Boolean { + // 暂时返回true,实际功能留空,但保持与iOS版本接口一致 + return true + } + + /** + * 初始化MCP客户端 + */ + fun initializeMcpClient(serverUrl: String): Boolean { + // MCP功能暂时留空,但保持与iOS版本接口一致 + return false + } + + /** + * MCP客户端是否已初始化 + */ + fun isMcpInitialized(): Boolean { + // MCP功能暂时留空,但保持与iOS版本接口一致 + return false + } + + /** + * 关闭MCP客户端 + */ + fun closeMcpClient() { + // MCP功能暂时留空,但保持与iOS版本接口一致 + } + + /** + * 处理MCP工具调用 + */ + suspend fun handleMcpToolCall(functionCallJson: String): String { + // MCP功能暂时留空,但保持与iOS版本接口一致 + return "MCP功能暂未实现" + } + + // MARK: - 工具方法 + + /** + * 检查消息中是否包含图片 + */ + private fun containsImage(messages: List>): Boolean { + for (message in messages) { + val content = message["content"] + if (content is String && content.contains("image_url")) { + return true + } + if (content is List<*>) { + content.forEach { item -> + if (item is Map<*, *>) { + val type = item["type"] as? String + if (type == "image_url") { + return true + } + } + } + } + } + return false + } + + /** + * 转换为OpenAI ChatMessage格式 + */ + private fun convertToChatMessages(messages: List>): List { + val chatMessages = mutableListOf() + + for (message in messages) { + val role = message["role"] as? String ?: continue + + val chatRole = when (role) { + "system" -> Role.System + "user" -> Role.User + "assistant" -> Role.Assistant + "tool" -> Role.Tool + else -> Role.User + } + + // 特殊处理assistant角色的tool_calls消息 + if (role == "assistant" && message.containsKey("tool_calls")) { + val content = message["content"] as? String ?: "" + + // 解析tool_calls + val toolCallsList = mutableListOf() + @Suppress("UNCHECKED_CAST") + val toolCallsArray = message["tool_calls"] as? List> + + toolCallsArray?.forEach { toolCallDict -> + val id = toolCallDict["id"] as? String ?: return@forEach + val type = toolCallDict["type"] as? String + if (type == "function") { + @Suppress("UNCHECKED_CAST") + val function = toolCallDict["function"] as? Map ?: return@forEach + val name = function["name"] as? String ?: return@forEach + val arguments = function["arguments"] as? String ?: "" + + val functionCall = FunctionCall( + nameOrNull = name, + argumentsOrNull = arguments + ) + + val toolCall = ToolCall.Function( + id = ToolId(id), + function = functionCall + ) + + toolCallsList.add(toolCall) + } + } + + val chatMessage = ChatMessage.Assistant( + content = if (content.isEmpty()) null else content, + toolCalls = if (toolCallsList.isEmpty()) null else toolCallsList + ) + + chatMessages.add(chatMessage) + continue + } + + // 特殊处理tool角色消息 + if (role == "tool") { + val content = message["content"] as? String ?: "" + val toolCallId = message["tool_call_id"] as? String ?: "" + + val chatMessage = ChatMessage.Tool( + content = content, + toolCallId = ToolId(toolCallId) + ) + + chatMessages.add(chatMessage) + continue + } + + // 处理普通消息的不同类型内容 + val content = message["content"] + when (content) { + is String -> { + val chatMessage = when (chatRole) { + Role.System -> ChatMessage.System(content) + Role.User -> ChatMessage.User(content) + Role.Assistant -> ChatMessage.Assistant(content) + else -> ChatMessage.User(content) + } + chatMessages.add(chatMessage) + } + is List<*> -> { + // 对于多模态内容,提取文本部分(简化处理) + var textContent = "" + + content.forEach { item -> + if (item is Map<*, *>) { + val type = item["type"] as? String + if (type == "text") { + val text = item["text"] as? String ?: "" + textContent += text + } + } + } + + val finalContent = if (textContent.isEmpty()) "多媒体内容" else textContent + + val chatMessage = when (chatRole) { + Role.System -> ChatMessage.System(finalContent) + Role.User -> ChatMessage.User(finalContent) + Role.Assistant -> ChatMessage.Assistant(finalContent) + else -> ChatMessage.User(finalContent) + } + chatMessages.add(chatMessage) + } + } + } + + return chatMessages + } + + /** + * 将文件转换为Base64字符串 + */ + fun fileToBase64(filePath: String, maxSizeKB: Int = 20480): String? { + return try { + val file = File(filePath) + if (!file.exists() || !file.isFile) { + return null + } + + // 读取文件 + var bitmap = BitmapFactory.decodeFile(filePath) ?: return null + + // 检查图片尺寸,限制最大为1024*1024 + val maxDimension = 1024 + if (bitmap.width > maxDimension || bitmap.height > maxDimension) { + // 计算缩放比例,保持纵横比 + val ratio = min(maxDimension.toFloat() / bitmap.width, maxDimension.toFloat() / bitmap.height) + + val newWidth = (bitmap.width * ratio).toInt() + val newHeight = (bitmap.height * ratio).toInt() + + bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) + } + + // 压缩图片 + val outputStream = ByteArrayOutputStream() + var quality = 100 + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) + + // 如果原始图片太大,继续优化文件大小 + var fileSize = file.length() / 1024 // 转为KB + if (fileSize > maxSizeKB) { + val scale = sqrt(maxSizeKB.toDouble() / fileSize) + val newWidth = (bitmap.width * scale).toInt() + val newHeight = (bitmap.height * scale).toInt() + bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) + outputStream.reset() + quality = 85 + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) + } + + // 转为Base64 + Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) + } catch (e: Exception) { + null + } + } + + /** + * 创建带图片的用户消息 + */ + fun createUserMessageWithImage(text: String, imageBase64: String): Map { + val contentArray = mutableListOf>() + + // 添加文本部分 + if (text.isNotEmpty()) { + contentArray.add( + mapOf( + "type" to "text", + "text" to text + ) + ) + } + + // 添加图片部分 + contentArray.add( + mapOf( + "type" to "image_url", + "image_url" to mapOf( + "url" to "data:image/jpeg;base64,$imageBase64" + ) + ) + ) + + return mapOf( + "role" to "user", + "content" to contentArray + ) + } + + /** + * 从URL中提取主机信息 (保留向后兼容) + */ + private fun extractHost(url: String): String { + return try { + val javaUrl = java.net.URL(url) + javaUrl.host ?: "api.openai.com" + } catch (e: Exception) { + "api.openai.com" + } + } + + /** + * 解析JSON参数 (与 iOS 版本保持一致) + */ + private fun parseJsonArguments(argumentsJson: String): Map { + return try { + if (argumentsJson.isBlank()) { + emptyMap() + } else { + val jsonObject = org.json.JSONObject(argumentsJson) + convertJsonObjectToMap(jsonObject) + } + } catch (e: Exception) { + emptyMap() + } + } + + /** + * 转换JSONArray为List> + */ + private fun convertJsonArrayToList(jsonArray: org.json.JSONArray): List> { + val result = mutableListOf>() + + for (i in 0 until jsonArray.length()) { + val jsonObject = jsonArray.optJSONObject(i) ?: continue + val map = convertJsonObjectToMap(jsonObject) + result.add(map) + } + + return result + } + + /** + * 转换JSONObject为Map + */ + private fun convertJsonObjectToMap(jsonObject: org.json.JSONObject): Map { + val map = mutableMapOf() + + val keys = jsonObject.keys() + while (keys.hasNext()) { + val key = keys.next() + val value = jsonObject.get(key) + + when (value) { + is org.json.JSONObject -> map[key] = convertJsonObjectToMap(value) + is org.json.JSONArray -> { + val list = mutableListOf() + for (i in 0 until value.length()) { + val item = value.get(i) + when (item) { + is org.json.JSONObject -> list.add(convertJsonObjectToMap(item)) + else -> list.add(item) + } + } + map[key] = list + } + else -> map[key] = value + } + } + + return map + } + + /** + * 转换Map为JSONObject + */ + private fun convertMapToJsonObject(map: Map): org.json.JSONObject { + val jsonObject = org.json.JSONObject() + + for ((key, value) in map) { + when (value) { + is Map<*, *> -> { + @Suppress("UNCHECKED_CAST") + jsonObject.put(key, convertMapToJsonObject(value as Map)) + } + is List<*> -> { + val jsonArray = org.json.JSONArray() + value.forEach { item -> + when (item) { + is Map<*, *> -> { + @Suppress("UNCHECKED_CAST") + jsonArray.put(convertMapToJsonObject(item as Map)) + } + else -> jsonArray.put(item) + } + } + jsonObject.put(key, jsonArray) + } + else -> jsonObject.put(key, value) + } + } + + return jsonObject + } +} \ No newline at end of file diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index c8b72a92b..bc9fc061e 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift @@ -340,17 +340,19 @@ public class SystemFunctionHandler: NSObject { // 检查位置服务是否可用 guard CLLocationManager.locationServicesEnabled() else { - continuation.resume(returning: Result(success: false, message: "位置服务未启用")) + locationCompletion?(Result(success: false, message: "位置服务未启用。请前往 设置 > 隐私与安全 > 定位服务 启用定位服务")) + locationCompletion = nil return } // 确保locationManager已初始化 guard let locationManager = locationManager else { - continuation.resume(returning: Result(success: false, message: "位置管理器未初始化")) + locationCompletion?(Result(success: false, message: "位置管理器未初始化")) + locationCompletion = nil return } - // 检查位置权限 - 兼容iOS 13.0+ + // 检查位置权限 let authStatus: CLAuthorizationStatus if #available(iOS 14.0, *) { authStatus = locationManager.authorizationStatus @@ -358,20 +360,27 @@ public class SystemFunctionHandler: NSObject { authStatus = CLLocationManager.authorizationStatus() } + print("当前位置权限状态: \(authStatus.rawValue)") + switch authStatus { case .notDetermined: - // 请求权限,权限结果会在 didChangeAuthorization 回调中处理 + print("请求位置权限...") + // 请求权限 locationManager.requestWhenInUseAuthorization() - // 不要在这里 return,让权限回调来处理后续逻辑 - case .denied, .restricted: - continuation.resume(returning: Result(success: false, message: "位置权限被拒绝")) - return + // 权限结果会在 didChangeAuthorization 回调中处理 + case .denied: + locationCompletion?(Result(success: false, message: "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限")) + locationCompletion = nil + case .restricted: + locationCompletion?(Result(success: false, message: "位置权限受限制,可能是由于家长控制或企业配置限制")) + locationCompletion = nil case .authorizedWhenInUse, .authorizedAlways: + print("已有位置权限,开始获取位置...") // 已有权限,直接请求位置 locationManager.requestLocation() @unknown default: - continuation.resume(returning: Result(success: false, message: "未知的位置权限状态")) - return + locationCompletion?(Result(success: false, message: "未知的位置权限状态: \(authStatus.rawValue)")) + locationCompletion = nil } } } @@ -404,10 +413,17 @@ extension SystemFunctionHandler: CLLocationManagerDelegate { return } - if status == .authorizedWhenInUse || status == .authorizedAlways { + switch status { + case .authorizedWhenInUse, .authorizedAlways: manager.requestLocation() - } else if status == .denied || status == .restricted { - locationCompletion?(Result(success: false, message: "位置权限被拒绝")) + case .denied, .restricted: + locationCompletion?(Result(success: false, message: "位置权限被拒绝。请前往 设置 > 隐私与安全 > 定位服务 > Deep Voice 启用定位权限")) + locationCompletion = nil + case .notDetermined: + // 权限仍未确定,等待用户操作 + break + @unknown default: + locationCompletion?(Result(success: false, message: "位置权限状态异常,请重试")) locationCompletion = nil } } diff --git a/local_plugins/chat_api/pubspec.yaml b/local_plugins/chat_api/pubspec.yaml index 297ec2147..4bc40daee 100644 --- a/local_plugins/chat_api/pubspec.yaml +++ b/local_plugins/chat_api/pubspec.yaml @@ -34,6 +34,9 @@ flutter: # adding or updating assets for this project. plugin: platforms: + android: + package: com.yunqiinnovation.chat_api + pluginClass: ChatApiPlugin ios: pluginClass: ChatApiPlugin ffiPlugin: true diff --git a/local_plugins/jl_ota/.gitignore b/local_plugins/jl_ota/.gitignore deleted file mode 100644 index e7d347d9d..000000000 --- a/local_plugins/jl_ota/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -build/ diff --git a/local_plugins/jl_ota/.metadata b/local_plugins/jl_ota/.metadata deleted file mode 100644 index 6015b831a..000000000 --- a/local_plugins/jl_ota/.metadata +++ /dev/null @@ -1,33 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "ea121f8859e4b13e47a8f845e4586164519588bc" - channel: "stable" - -project_type: plugin - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: ea121f8859e4b13e47a8f845e4586164519588bc - base_revision: ea121f8859e4b13e47a8f845e4586164519588bc - - platform: android - create_revision: ea121f8859e4b13e47a8f845e4586164519588bc - base_revision: ea121f8859e4b13e47a8f845e4586164519588bc - - platform: ios - create_revision: ea121f8859e4b13e47a8f845e4586164519588bc - base_revision: ea121f8859e4b13e47a8f845e4586164519588bc - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/local_plugins/jl_ota/CHANGELOG.md b/local_plugins/jl_ota/CHANGELOG.md deleted file mode 100644 index 41cc7d819..000000000 --- a/local_plugins/jl_ota/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/local_plugins/jl_ota/LICENSE b/local_plugins/jl_ota/LICENSE deleted file mode 100644 index ba75c69f7..000000000 --- a/local_plugins/jl_ota/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/local_plugins/jl_ota/README.md b/local_plugins/jl_ota/README.md deleted file mode 100644 index 673c7a29a..000000000 --- a/local_plugins/jl_ota/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# jl_ota - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/to/develop-plugins), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev), which offers tutorials, -samples, guidance on mobile development, and a full API reference. - diff --git a/local_plugins/jl_ota/analysis_options.yaml b/local_plugins/jl_ota/analysis_options.yaml deleted file mode 100644 index a5744c1cf..000000000 --- a/local_plugins/jl_ota/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -include: package:flutter_lints/flutter.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/local_plugins/jl_ota/android/.gitignore b/local_plugins/jl_ota/android/.gitignore deleted file mode 100644 index 161bdcdaf..000000000 --- a/local_plugins/jl_ota/android/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -.cxx diff --git a/local_plugins/jl_ota/android/build.gradle b/local_plugins/jl_ota/android/build.gradle deleted file mode 100644 index dd9b88141..000000000 --- a/local_plugins/jl_ota/android/build.gradle +++ /dev/null @@ -1,68 +0,0 @@ -group = "com.yunqiinnovation.jl_ota" -version = "1.0-SNAPSHOT" - -buildscript { - ext.kotlin_version = "1.8.22" - repositories { - google() - mavenCentral() - } - - dependencies { - classpath("com.android.tools.build:gradle:8.7.0") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: "com.android.library" -apply plugin: "kotlin-android" - -android { - namespace = "com.yunqiinnovation.jl_ota" - - compileSdk = 35 - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11 - } - - sourceSets { - main.java.srcDirs += "src/main/kotlin" - test.java.srcDirs += "src/test/kotlin" - } - - defaultConfig { - minSdk = 21 - } - - dependencies { - // implementation project(':ble_service') - - testImplementation("org.jetbrains.kotlin:kotlin-test") - testImplementation("org.mockito:mockito-core:5.0.0") - } - - testOptions { - unitTests.all { - useJUnitPlatform() - - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} \ No newline at end of file diff --git a/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.2-release.aar b/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.2-release.aar deleted file mode 100644 index 9ecf85489..000000000 Binary files a/local_plugins/jl_ota/android/libs/jl_bt_ota_V1.9.2-release.aar and /dev/null differ diff --git a/local_plugins/jl_ota/android/settings.gradle b/local_plugins/jl_ota/android/settings.gradle deleted file mode 100644 index 0ff55ddd6..000000000 --- a/local_plugins/jl_ota/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'jl_ota' \ No newline at end of file diff --git a/local_plugins/jl_ota/android/src/main/AndroidManifest.xml b/local_plugins/jl_ota/android/src/main/AndroidManifest.xml deleted file mode 100644 index efb57f32c..000000000 --- a/local_plugins/jl_ota/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt b/local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt deleted file mode 100644 index 9a76c5a11..000000000 --- a/local_plugins/jl_ota/android/src/main/kotlin/com/yunqiinnovation/jl_ota/JlOtaPlugin.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.yunqiinnovation.jl_ota - -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result - -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothDevice -import android.content.Context -import android.os.Handler -import android.os.Looper -import android.util.Log -import androidx.annotation.NonNull -//import com.yunqiinnovation.ble_service.BleService - -/** JlOtaPlugin */ -class JlOtaPlugin() : FlutterPlugin, MethodCallHandler { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel: MethodChannel - private lateinit var context: Context - private lateinit var eventChannel: EventChannel - private var streamSink: EventChannel.EventSink? = null - - companion object { - private const val TAG = "JlOtaPlugin" - private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" - private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" - - } - - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - context = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "jl_ota") - channel.setMethodCallHandler(this) - - eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME) - - eventChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - streamSink = events - } - - override fun onCancel(arguments: Any?) { - streamSink = null - } - }) - - } - - override fun onMethodCall(call: MethodCall, result: Result) { - - if (call.method == "getPlatformVersion") { - - // BleService.otaManager?.configureOTA() - - // BleService.otaManager?.startOTA() - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } else { - result.notImplemented() - } - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } - - -} - diff --git a/local_plugins/jl_ota/ios/.gitignore b/local_plugins/jl_ota/ios/.gitignore deleted file mode 100644 index 034771fc9..000000000 --- a/local_plugins/jl_ota/ios/.gitignore +++ /dev/null @@ -1,38 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/ephemeral/ -/Flutter/flutter_export_environment.sh diff --git a/local_plugins/jl_ota/ios/Assets/.gitkeep b/local_plugins/jl_ota/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/local_plugins/jl_ota/ios/Classes/JlOtaPlugin.swift b/local_plugins/jl_ota/ios/Classes/JlOtaPlugin.swift deleted file mode 100644 index e5b6434f2..000000000 --- a/local_plugins/jl_ota/ios/Classes/JlOtaPlugin.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Flutter -import UIKit - -public class JlOtaPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "jl_ota", binaryMessenger: registrar.messenger()) - let instance = JlOtaPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "getPlatformVersion": - result("iOS " + UIDevice.current.systemVersion) - default: - result(FlutterMethodNotImplemented) - } - } -} diff --git a/local_plugins/jl_ota/ios/Resources/PrivacyInfo.xcprivacy b/local_plugins/jl_ota/ios/Resources/PrivacyInfo.xcprivacy deleted file mode 100644 index a34b7e2e6..000000000 --- a/local_plugins/jl_ota/ios/Resources/PrivacyInfo.xcprivacy +++ /dev/null @@ -1,14 +0,0 @@ - - - - - NSPrivacyTrackingDomains - - NSPrivacyAccessedAPITypes - - NSPrivacyCollectedDataTypes - - NSPrivacyTracking - - - diff --git a/local_plugins/jl_ota/ios/jl_ota.podspec b/local_plugins/jl_ota/ios/jl_ota.podspec deleted file mode 100644 index ebd8d61ed..000000000 --- a/local_plugins/jl_ota/ios/jl_ota.podspec +++ /dev/null @@ -1,29 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint jl_ota.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'jl_ota' - s.version = '0.0.1' - s.summary = 'A new Flutter project.' - s.description = <<-DESC -A new Flutter project. - 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.dependency 'Flutter' - s.platform = :ios, '12.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.0' - - # If your plugin requires a privacy manifest, for example if it uses any - # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your - # plugin's privacy impact, and then uncomment this line. For more information, - # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files - # s.resource_bundles = {'jl_ota_privacy' => ['Resources/PrivacyInfo.xcprivacy']} -end diff --git a/local_plugins/jl_ota/lib/jl_ota.dart b/local_plugins/jl_ota/lib/jl_ota.dart deleted file mode 100644 index ed1f427bd..000000000 --- a/local_plugins/jl_ota/lib/jl_ota.dart +++ /dev/null @@ -1,8 +0,0 @@ - -import 'jl_ota_platform_interface.dart'; - -class JlOta { - Future getPlatformVersion() { - return JlOtaPlatform.instance.getPlatformVersion(); - } -} diff --git a/local_plugins/jl_ota/lib/jl_ota_method_channel.dart b/local_plugins/jl_ota/lib/jl_ota_method_channel.dart deleted file mode 100644 index aa4647443..000000000 --- a/local_plugins/jl_ota/lib/jl_ota_method_channel.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import 'jl_ota_platform_interface.dart'; - -/// An implementation of [JlOtaPlatform] that uses method channels. -class MethodChannelJlOta extends JlOtaPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - final methodChannel = const MethodChannel('jl_ota'); - - @override - Future getPlatformVersion() async { - final version = await methodChannel.invokeMethod('getPlatformVersion'); - return version; - } -} diff --git a/local_plugins/jl_ota/lib/jl_ota_platform_interface.dart b/local_plugins/jl_ota/lib/jl_ota_platform_interface.dart deleted file mode 100644 index f4c504da7..000000000 --- a/local_plugins/jl_ota/lib/jl_ota_platform_interface.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'jl_ota_method_channel.dart'; - -abstract class JlOtaPlatform extends PlatformInterface { - /// Constructs a JlOtaPlatform. - JlOtaPlatform() : super(token: _token); - - static final Object _token = Object(); - - static JlOtaPlatform _instance = MethodChannelJlOta(); - - /// The default instance of [JlOtaPlatform] to use. - /// - /// Defaults to [MethodChannelJlOta]. - static JlOtaPlatform get instance => _instance; - - /// Platform-specific implementations should set this with their own - /// platform-specific class that extends [JlOtaPlatform] when - /// they register themselves. - static set instance(JlOtaPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - Future getPlatformVersion() { - throw UnimplementedError('platformVersion() has not been implemented.'); - } -} diff --git a/local_plugins/jl_ota/pubspec.yaml b/local_plugins/jl_ota/pubspec.yaml deleted file mode 100644 index 3684363bf..000000000 --- a/local_plugins/jl_ota/pubspec.yaml +++ /dev/null @@ -1,72 +0,0 @@ -name: jl_ota -description: "A new Flutter project." -version: 0.0.1 -homepage: - -environment: - sdk: ^3.7.0 - flutter: '>=3.3.0' - -dependencies: - flutter: - sdk: flutter - plugin_platform_interface: ^2.0.2 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^5.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) - # which should be registered in the plugin registry. This is required for - # using method channels. - # The Android 'package' specifies package in which the registered class is. - # This is required for using method channels on Android. - # The 'ffiPlugin' specifies that native code should be built and bundled. - # This is required for using `dart:ffi`. - # All these are used by the tooling to maintain consistency when - # adding or updating assets for this project. - plugin: - platforms: - android: - package: com.yunqiinnovation.jl_ota - pluginClass: JlOtaPlugin - ios: - pluginClass: JlOtaPlugin - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/to/asset-from-package - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/to/font-from-package diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock deleted file mode 100644 index 8ed41b458..000000000 Binary files a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties deleted file mode 100644 index c231039ee..000000000 --- a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties +++ /dev/null @@ -1,2 +0,0 @@ -#Wed May 21 01:03:35 CST 2025 -gradle.version=8.9 diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/DeviceConnection.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/DeviceConnection.kt deleted file mode 100644 index 0a9abdb19..000000000 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/DeviceConnection.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.jieli.otasdk.model - -import android.bluetooth.BluetoothDevice - -/** - * @author zqjasonZhong - * @since 2022/9/9 - * @email zhongzhuocheng@zh-jieli.com - * @desc 设备连接状态 - */ -class DeviceConnection(val device: BluetoothDevice?, var state: Int) { - - override fun hashCode(): Int { - return device.hashCode() - } - - override fun equals(other: Any?): Boolean { - if (other !is DeviceConnection) return false - return device == other.device - } - - override fun toString(): String { - return "DeviceConnection(device=$device, state=$state)" - } - -} \ No newline at end of file diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt index cabac136a..c9d268c39 100644 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt @@ -20,129 +20,365 @@ import com.jieli.jl_bt_ota.util.CHexConver import com.jieli.jl_bt_ota.util.JL_Log import com.jieli.otasdk.tool.bluetooth.BluetoothHelper import androidx.lifecycle.MutableLiveData -import com.jieli.otasdk.model.DeviceConnection import android.util.Log import androidx.annotation.NonNull import com.jieli.otasdk.tool.config.ConfigHelper import com.yunqiinnovation.ble_service.BleService - class CustomUpdateCallback( +import com.yunqiinnovation.ble_service.BleConst +import android.os.Handler +import android.os.Looper - val device: BluetoothDevice? +import java.io.File +import java.io.FileOutputStream +import java.io.RandomAccessFile +import io.flutter.plugin.common.EventChannel + +class CustomUpdateCallback( + + val device: BluetoothDevice?, + private val streamSink: EventChannel.EventSink? ) : IUpgradeCallback { companion object { private const val TAG = "JlOtaPlugin" - private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" - private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" + private const val CHANNEL_NAME = "com.example.ota" + private const val EVENT_CHANNEL_NAME = "com.example.ota/events" } override fun onStartOTA() { Log.d(TAG, "onStartOTA开始升级") - // viewModel.otaStateMLD.value = OTAStart(device) + if (streamSink != null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + streamSink.success( + mapOf( + "event" to "onStartOTA" + ) + ) + } + } } override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) { Log.d(TAG, "onNeedReconnect需要重新链接") - + if (streamSink != null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + streamSink.success( + mapOf( + "event" to "onNeedReconnect", + "addr" to addr, + "isNewReconnectWay" to isNewReconnectWay + ) + ) + } + } } override fun onProgress(type: Int, progress: Float) { Log.d(TAG, "onProgress正在进展") - //viewModel.otaStateMLD.value = OTAWorking(device, type, progress) + if (streamSink != null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + streamSink.success( + mapOf( + "event" to "onProgress", + "type" to type, + "progress" to progress + ) + ) + } else { + // Handler(Looper.getMainLooper()).post { + // streamSink.success( + // mapOf( + // "event" to "onDecodeStream" + // //"data" to data + // ) + // ) + // } + } + } } override fun onStopOTA() { Log.d(TAG, "onStartOTA停止升级") - + if (streamSink != null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + streamSink.success( + mapOf( + "event" to "onStopOTA", + ) + ) + } + } + BleService.isEnterOta = false } override fun onCancelOTA() { Log.d(TAG, "onCancelOTA取消升级") - + if (streamSink != null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + streamSink.success( + mapOf( + "event" to "onCancelOTA", + ) + ) + } + } } override fun onError(error: BaseError?) { val errorMessage = error?.message ?: "Unknown error" - Log.e(TAG, "升级失败: ${error?.message ?: "未知错误"}") - + Log.d(TAG, "升级失败: ${error?.message ?: "未知错误"}") + if (streamSink != null) { + if (Looper.myLooper() == Looper.getMainLooper()) { + Log.d(TAG, "升级失败:error?.message ${error?.message}未知错误 error?.code${error?.code}") + streamSink.success( + mapOf( + "event" to "onError", + "code" to (error?.code ?: 255), // 确保值是正确的类型 + "message" to (error?.message ?: "Unknown error") // 确保值是正确的类型 + ) + ) + } + } // 如果需要打印完整错误对象信息(假设BaseError有重写toString()) Log.d(TAG, "完整错误对象: ${error ?: "null"}") } } /** OtaPlugin */ -class OtaPlugin: FlutterPlugin, MethodCallHandler { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel : MethodChannel - private lateinit var context: Context - -private lateinit var bluetoothHelper : BluetoothHelper - val otaConnectionMLD = MutableLiveData() - val mandatoryUpgradeMLD = MutableLiveData() - private lateinit var otaManager : OTAManager - - private val bluetoothCallback = object : BtEventCallback() { +class OtaPlugin : BleService.Callback, FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + private lateinit var context: Context + private lateinit var otaDevice: BluetoothDevice + private lateinit var bluetoothHelper: BluetoothHelper + + private lateinit var otaManager: OTAManager + private lateinit var filePath: String//升级文件路径 + private var isDisconnect = false//是否主动断开 + private var streamSink: EventChannel.EventSink? = null + private lateinit var eventChannel: EventChannel + + // 是否已注册BLE回调 + private var isRegisteredToBle = false + + // 添加主线程Handler用于确保在主线程回调 + private val mainHandler = Handler(Looper.getMainLooper()) + + companion object { + private const val TAG = "OtaPlugin" + private const val CHANNEL_NAME = "com.example.ota" + private const val EVENT_CHANNEL_NAME = "com.example.ota/events" + + } + + // 在主线程上安全发送事件 + private fun sendEventOnMainThread(event: Map) { + if (streamSink == null) return + + if (Looper.myLooper() == Looper.getMainLooper()) { + // 如果已经在主线程,直接发送 + streamSink?.success(event) + } else { + // 否则切换到主线程 + mainHandler.post { + streamSink?.success(event) + } + } + } + + // 在主线程上安全返回结果 + private fun sendResultOnMainThread(result: Result, value: Any?) { + if (Looper.myLooper() == Looper.getMainLooper()) { + // 如果已经在主线程,直接返回 + result.success(value) + } else { + // 否则切换到主线程 + mainHandler.post { + result.success(value) + } + } + } + + // 在主线程上安全返回错误 + private fun sendErrorOnMainThread( + result: Result, + errorCode: String, + errorMessage: String, + errorDetails: Any? + ) { + if (Looper.myLooper() == Looper.getMainLooper()) { + // 如果已经在主线程,直接返回 + result.error(errorCode, errorMessage, errorDetails) + } else { + // 否则切换到主线程 + mainHandler.post { + result.error(errorCode, errorMessage, errorDetails) + } + } + } + + private val bluetoothCallback = object : BtEventCallback() { override fun onConnection(device: BluetoothDevice?, status: Int) { if ((status == StateCode.CONNECTION_FAILED || status == StateCode.CONNECTION_DISCONNECT) && bluetoothHelper.getConnectedDevice() != null) { bluetoothHelper.disconnectDevice(bluetoothHelper.getConnectedDevice()) + + } + otaDevice = device!! + // sendEventOnMainThread( + // mapOf( + // "event" to "NOCONNECTION" + // ) + // ) + // 更新连接状态的 LiveData,用于通知 UI 或其他逻辑 + // otaConnectionMLD.value = DeviceConnection(device, status) + Log.d("bluetoothCallback", "onConnection,status${status}") + if (status == StateCode.CONNECTION_OK) { + Log.d("TAG", "startUpgrade") + + sendEventOnMainThread( + mapOf( + "event" to "CONNECTION_SUCCESS" + ) + ) + Log.d("TAG", "startUpgrade,filePath=${filePath}") + otaManager.bluetoothOption.firmwareFilePath = filePath + otaManager.startOTA( + CustomUpdateCallback( + otaManager.bluetoothHelper.getConnectedDevice(), streamSink + ) + ) } - otaConnectionMLD.value = DeviceConnection(device, status) } override fun onMandatoryUpgrade(device: BluetoothDevice?) { - mandatoryUpgradeMLD.postValue(device!!) + // mandatoryUpgradeMLD.postValue(device!!) + Log.d("bluetoothCallback", "onMandatoryUpgrade") } } - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { context = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "ota") - channel.setMethodCallHandler(this) - - Log.d("TAG", "onAttachedToEngine") - } - - override fun onMethodCall(call: MethodCall, result: Result) { - if (call.method == "getPlatformVersion") { - Log.d("TAG", "Connection") - // otaManager.configHelper - // 确保先初始化 ConfigHelper + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "ota") + channel.setMethodCallHandler(this) + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME) + + eventChannel.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + streamSink = events + } + + override fun onCancel(arguments: Any?) { + streamSink = null + } + }) + // 初始化 otaManager 和 bluetoothHelper ConfigHelper.initialize(context) - otaManager = OTAManager(context) - bluetoothHelper = BluetoothHelper(context) - // 如果需要额外的初始化逻辑,请在这里添加 - otaManager.registerBluetoothCallback(bluetoothCallback) - - // otaManager.registerBluetoothCallback(bluetoothCallback) - result.success("Android ${android.os.Build.VERSION.RELEASE}") - }else if (call.method == "searchDevices") { - Log.d("TAG", "searchDevices") - // otaManager.configHelper - BleService.disconnect() - // 确保先初始化 ConfigHelper - otaManager.bluetoothHelper.startScan(BleService.bluetoothGatt?.device) - - // otaManager.registerBluetoothCallback(bluetoothCallback) - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } else if (call.method == "startUpgrade") { - Log.d("TAG", "startUpgrade") - var filePath = "/storage/emulated/0/Download/JieLiOTA/upgrade/update - v0.1.0_添加杰理OTA,优化主从切换拾音丢失问题.ufw" - otaManager.bluetoothOption.firmwareFilePath = filePath - otaManager.startOTA( - CustomUpdateCallback( - otaManager.bluetoothHelper.getConnectedDevice() - ) - ) - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } else { - result.notImplemented() - } - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } + otaManager = OTAManager(context) + bluetoothHelper = BluetoothHelper(context) + // 如果需要额外的初始化逻辑,请在这里添加 + otaManager.registerBluetoothCallback(bluetoothCallback) + BleService.addCallback(this) + isRegisteredToBle = true + Log.i("TAG", "onAttachedToEngine") + + } + + override fun onMethodCall(call: MethodCall, result: Result) { + if (call.method == "startUpgrading") { + Log.d("TAG", "startUpgrading") + // 接收传入的参数 + val arguments = call.arguments as? Map + filePath = (arguments?.get("filePath") as? String).toString() + + if (filePath != null) { + Log.d("TAG", "Received filePath: $filePath") + // 在这里使用 filePath 参数,例如传递给其他方法或类 + } else { + Log.d("TAG", "filePath 参数为空") + } + BleService.isEnterOta = true + + val bluetoothGatt = BleService.bluetoothGatt + if (bluetoothGatt?.device != null) { + isDisconnect = true + otaDevice = bluetoothGatt.device + BleService.disconnect() + + } else if(otaDevice != null) + { isDisconnect = true + BleService.disconnect() + } + else{ + Log.e("TAG", "BluetoothGatt 或 device 为 null,无法执行操作") + sendEventOnMainThread( + mapOf( + "event" to "NOCONNECTION" + ) + ) + BleService.isEnterOta = false + + } + + + } else if (call.method == "onOtaClose") { + BleService.isEnterOta = false + isDisconnect = false + otaManager.bluetoothHelper.stopScan() + otaManager.bluetoothHelper.disconnectDevice(otaDevice) + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + eventChannel.setStreamHandler(null) + // 取消BLE回调注册 + if (isRegisteredToBle) { + BleService.removeCallback(this) + isRegisteredToBle = false + } + isDisconnect = false + // 释放其他资源 + streamSink = null + BleService.isEnterOta = false + Log.d(TAG, "OtaPlugin 已释放资源") + } + + + override fun onScanResult(devices: List>) { + // 不处理扫描结果 + } + + override fun onConnectionStateChanged(state: Int) { + if (state == BleConst.STATE_DISCONNECTED && isDisconnect) { + Log.d("TAG", "Ble设备已断开,onConnectionStateChanged") + Handler(Looper.getMainLooper()).postDelayed({ + otaManager.bluetoothHelper.startScan(otaDevice) + }, 2000) // 2000 毫秒 = 2 秒 + } + } + + override fun onAudioDataReceived(data: ByteArray) { + + // 可选:处理音频数据 + } + + /** + * 处理唤醒信号 + * 在收到唤醒信号时启动语音识别 + */ + override fun onWakeupSignalReceived() { + + } + + override fun onDeviceInfoReceived(infoType: Int, infoData: Map) { + // 不处理设备信息 + } + + } diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt index 10d86a089..8001b29ee 100644 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt @@ -16,19 +16,41 @@ import com.jieli.otasdk.tool.config.ConfigHelper import com.jieli.otasdk.tool.bluetooth.OtaConstant import java.util.* import android.util.Log + /** * 用于RCSP的第三方SDK接入OTA流程 */ class OTAManager(context: Context) : BluetoothOTAManager(context) { - val configHelper = ConfigHelper.getInstance() - val bluetoothHelper = BluetoothHelper.getInstance(context) + val configHelper = ConfigHelper.getInstance() + val bluetoothHelper = BluetoothHelper.getInstance(context) + + /** + * 转换成OTA库的连接状态 + * + * @param status 连接状态 + * @return 库的连接状态 + */ + + fun changeConnectStatus(status: Int): Int { + var changeStatus = StateCode.CONNECTION_DISCONNECT + when (status) { + 2 -> { + changeStatus = StateCode.CONNECTION_OK + } + + 1 -> { + changeStatus = StateCode.CONNECTION_CONNECTING + } + } + return changeStatus + } - val btEventCallback = object : OnBTEventCallback() { + val btEventCallback = object : OnBTEventCallback() { override fun onDeviceConnection(device: BluetoothDevice?, way: Int, status: Int) { super.onDeviceConnection(device, way, status) - // val connectionState = AppUtil.changeConnectStatus(status) + val connectionState = changeConnectStatus(status) // Log.i( // TAG, "onConnection >>> device : ${printDeviceInfo(device)}, way = $way" + // " status :$status, change status : $connectionState" @@ -37,9 +59,9 @@ class OTAManager(context: Context) : BluetoothOTAManager(context) { //注释掉 Log.i( TAG, "onConnection >>> device : ${device}, way = $way" + - " status :$status, change status : 3" + " status :$status, change status : $connectionState" ) - onBtDeviceConnection(device, 1) + onBtDeviceConnection(device, connectionState) } override fun onReceiveData( @@ -70,18 +92,19 @@ class OTAManager(context: Context) : BluetoothOTAManager(context) { } init { - // ConfigHelper.initialize(context) + // ConfigHelper.initialize(context) val bluetoothOption = BluetoothOTAConfigure() //选择通讯方式 bluetoothOption.priority = BluetoothOTAConfigure.PREFER_BLE - + //是否需要自定义回连方式(默认不需要,如需要自定义回连方式,需要客户自行实现) bluetoothOption.isUseReconnect = (configHelper.isUseCustomReConnectWay() && configHelper.isHidDevice()) - Log.d( - TAG, "是否启用设备认证流程(与固件工程师确认) >>> ${configHelper.isUseDeviceAuth()} >>> ${bluetoothOption.isUseReconnect} " - ) + Log.d( + TAG, + "是否启用设备认证流程(与固件工程师确认) >>> ${configHelper.isUseDeviceAuth()} >>> ${bluetoothOption.isUseReconnect} " + ) //是否启用设备认证流程(与固件工程师确认) bluetoothOption.isUseAuthDevice = configHelper.isUseDeviceAuth() //设置BLE的MTU diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleManager.java b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleManager.java index feec079de..aa5708974 100644 --- a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleManager.java +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleManager.java @@ -88,7 +88,7 @@ public class BleManager { private volatile boolean isBleScanning; private NotifyCharacteristicRunnable mNotifyCharacteristicRunnable; - private BluetoothDevice deviced;//需要连接手机设备信息 + private BluetoothDevice deviced1;//需要连接手机设备信息 private long timeout = 15000;//扫描设备时间 private int mRetryConnectCount = 0;//连接失败后尝试连接次数 private long startConnectTime = 0; //开始连接时间戳 @@ -311,7 +311,8 @@ public class BleManager { } @SuppressLint("MissingPermission") public boolean startLeScan(BluetoothDevice deviced) { - Log.i(TAG, ", timeout = " + timeout); + deviced1=deviced; + Log.i(TAG, ", deviced = " + deviced); if (!isBluetoothEnable() || !isLocationPermissionGranted(mContext)) { checkPermissionsAndRequest(mContext); return false; @@ -632,7 +633,8 @@ public class BleManager { && !mDiscoveredBleDevices.contains(device)) { Log.d(TAG, "notify device : " + printDeviceInfo(device)); Log.d(TAG, "device.getAddress : " + device.getAddress()); - if ("4D:9B:09:38:22:F5".equals(device.getAddress())) { + Log.d(TAG, "deviced1.getAddress() : " + deviced1.getAddress()); + if (deviced1 != null && deviced1.getAddress().equals(device.getAddress())) { // 匹配成功 Log.i(TAG, "filterDevice找到了DeapSound设备"); @@ -644,9 +646,10 @@ public class BleManager { } else { Log.e(TAG, "连接设备失败: ${device.name} (${device.address})"); } - } + mDiscoveredBleDevices.add(device); handleDiscoveryBle(device, new BleScanInfo().setRawData(scanRecord).setRssi(rssi).setEnableConnect(isBleEnableConnect)); + } } } diff --git a/local_plugins/ota/lib/ota.dart b/local_plugins/ota/lib/ota.dart index d3d2606f1..e21eb0a8d 100644 --- a/local_plugins/ota/lib/ota.dart +++ b/local_plugins/ota/lib/ota.dart @@ -1,19 +1,142 @@ import 'ota_platform_interface.dart'; +import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter/services.dart'; + +class OtaEvent { + static final int NOT_STARTED = -1; // 未开始 + static final int VALIDATION_FILE = 0; // 验证文件 + static final int ONUPGRADE = 1; // 升级中 + static final int START_UPGRADING = 2; // 开始升级 + static final int UPGRADE_SUCCESS = 3; // 升级成功 + static final int UPGRADE_FAILED = 4; // 升级失败 + static final int CONNECTION_DISCONNECTION = 5; // 连接断开 + static final int CONNECTION_SUCCESS = 6; // 连接成功 + + /// 事件类型 + final String event; + + /// 输出升级状态 + final int? type; + + /// 输出升级进度 + final double? progress; + + /// 错误码 (仅在发生错误时有效) + final int? code; + + /// 错误信息 (仅在发生错误时有效) + final String? message; + + /// 构造函数 + OtaEvent({ + required this.event, + this.type, + this.progress, + this.code, + this.message, + }); + + /// 从Map构造对象 + factory OtaEvent.fromMap(Map map) { + return OtaEvent( + event: map['event'], + type: map['type'], + progress: map['progress'], + code: map['code'], + message: map['message'], + ); + } +} class Ota { - Future getPlatformVersion() { - return OtaPlatform.instance.getPlatformVersion(); + // 单例实例 + static final Ota _instance = Ota._internal(); + + // 私有构造函数 + Ota._internal() { + // 初始化时设置事件监听 + // _setupEventListener(); } - Future searchDevices() { - return OtaPlatform.instance.searchDevices(); + // 公共访问点 + static Ota get instance => _instance; + + static const EventChannel _eventChannel = EventChannel( + 'com.example.ota/events', + ); + // double progress = 0.0; + // late int type = OtaEvent.NOT_STARTED; + // bool isUpgrade = false; + + /// 事件流 + Stream? _eventStream; + // bool _isEventListenerSet = false; // 防止重复监听 + + /// 获取事件流 + Stream get eventStream { + _eventStream ??= _eventChannel.receiveBroadcastStream().map((event) { + if (event is Map) { + return OtaEvent.fromMap(event); + } + throw PlatformException(code: 'INVALID_EVENT', message: '无效的事件数据'); + }); + return _eventStream!; } - Future connectDevice() { - return OtaPlatform.instance.connectDevice(); + /// 释放资源 + void onClose() { + onOtaClose(); + // 停止事件监听 + _eventStream = null; + //_isEventListenerSet = false; + print("Ota 对象资源已释放"); + } + + // /// 内部设置事件监听(只执行一次) + // void _setupEventListener() { + // if (_isEventListenerSet) return; + + // eventStream.listen((event) { + // if (event.event == 'onProgress' && + // event.type != null && + // event.progress != null) { + // print("flutter 收到数据${event.progress}type=${type}"); + // progress = event.progress!; + // type = event.type!; + // } + // if (event.event == 'onStartOTA') { + // type = OtaEvent.START_UPGRADING; + // isUpgrade = true; // 开始升级 + // } + // if (event.event == 'onStopOTA') { + // type = OtaEvent.UPGRADE_SUCCESS; + // isUpgrade = false; + // } + // if (event.event == 'onError' && + // event.code != null && + // event.message != null) { + // print("flutter onError${event.code}type=${event.message}"); + // type = OtaEvent.UPGRADE_FAILED; + // isUpgrade = false; + // } + // if (event.event == 'NOCONNECTION') { + // type = OtaEvent.CONNECTION_DISCONNECTION; + // isUpgrade = false; + // } + // if (event.event == 'CONNECTION_SUCCESS') { + // type = OtaEvent.CONNECTION_SUCCESS; + // } + // }); + + // _isEventListenerSet = true; + // } + + Future startUpgrading(String? filePath) { + return OtaPlatform.instance.startUpgrading(filePath); } - Future startUpgrade() { - return OtaPlatform.instance.startUpgrade(); + Future onOtaClose() { + return OtaPlatform.instance.onOtaClose(); } } diff --git a/local_plugins/ota/lib/ota_method_channel.dart b/local_plugins/ota/lib/ota_method_channel.dart index 56e8fde86..504acb9d6 100644 --- a/local_plugins/ota/lib/ota_method_channel.dart +++ b/local_plugins/ota/lib/ota_method_channel.dart @@ -10,25 +10,17 @@ class MethodChannelOta extends OtaPlatform { final methodChannel = const MethodChannel('ota'); @override - Future getPlatformVersion() async { + Future startUpgrading(String? filePath) async { final version = await methodChannel.invokeMethod( - 'getPlatformVersion', + 'startUpgrading', + {'filePath': filePath}, // 传递参数 ); return version; } - Future searchDevices() async { - final version = await methodChannel.invokeMethod('searchDevices'); - return version; - } - - Future connectDevice() async { - final version = await methodChannel.invokeMethod('connectDevice'); - return version; - } - - Future startUpgrade() async { - final version = await methodChannel.invokeMethod('startUpgrade'); + @override + Future onOtaClose() async { + final version = await methodChannel.invokeMethod('onOtaClose'); return version; } } diff --git a/local_plugins/ota/lib/ota_platform_interface.dart b/local_plugins/ota/lib/ota_platform_interface.dart index 77ade98ad..369f2d8f0 100644 --- a/local_plugins/ota/lib/ota_platform_interface.dart +++ b/local_plugins/ota/lib/ota_platform_interface.dart @@ -23,19 +23,11 @@ abstract class OtaPlatform extends PlatformInterface { _instance = instance; } - Future getPlatformVersion() { + Future startUpgrading(String? filePath) { throw UnimplementedError('platformVersion() has not been implemented.'); } - Future searchDevices() { - throw UnimplementedError('platformVersion() has not been implemented.'); - } - - Future connectDevice() { - throw UnimplementedError('platformVersion() has not been implemented.'); - } - - Future startUpgrade() { + Future onOtaClose() { throw UnimplementedError('platformVersion() has not been implemented.'); } } diff --git a/local_plugins/ota/pubspec.yaml b/local_plugins/ota/pubspec.yaml index cb281e3be..7db9dc8a6 100644 --- a/local_plugins/ota/pubspec.yaml +++ b/local_plugins/ota/pubspec.yaml @@ -11,12 +11,11 @@ dependencies: flutter: sdk: flutter plugin_platform_interface: ^2.0.2 - dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 - + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/pubspec.yaml b/pubspec.yaml index cae202efa..58beb5f36 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,8 +79,6 @@ dependencies: path: local_plugins/spotify_sdk ota: path: local_plugins/ota - jl_ota: - path: local_plugins/jl_ota ble_service: path: local_plugins/ble_service classic_bluetooth: