102 changed files with 7937 additions and 1141 deletions
@ -0,0 +1,28 @@ |
|||
import 'dart:convert'; |
|||
|
|||
import 'package:deep_voice/data/models/appconfig_model.dart'; |
|||
|
|||
class AppConfig { |
|||
static late UserGetAppConfigResp config; |
|||
|
|||
// 可选:提供初始化方法 |
|||
static void initialize(UserGetAppConfigResp resp) { |
|||
config = resp; |
|||
} |
|||
|
|||
//环境变量 |
|||
static String? env(String key) { |
|||
return config.env[key]; |
|||
} |
|||
|
|||
//mcp配置 |
|||
static dynamic mcpConfig() { |
|||
return {"mcpServers": config.mcps}; |
|||
} |
|||
|
|||
//智能体配置 |
|||
static String agent(String agentId) { |
|||
final foundAgent = config.agents.firstWhere((agent) => agent.id == agentId); |
|||
return jsonEncode((foundAgent)); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
import 'package:deep_voice/core/utils/logger.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:music_service/music_service.dart'; |
|||
|
|||
//音乐播放管理 |
|||
class MusicManager extends GetxService { |
|||
static MusicManager get to => Get.find<MusicManager>(); |
|||
|
|||
// 音乐服务实例 |
|||
final MusicService _musicService = MusicService(); |
|||
|
|||
/// 初始化 |
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
Logger.info('BLE管理器初始化'); |
|||
} |
|||
|
|||
/// 释放资源 |
|||
@override |
|||
void onClose() { |
|||
super.onClose(); |
|||
} |
|||
|
|||
//播放 音乐 |
|||
Future<void> play(String url) async {} |
|||
} |
|||
@ -1,4 +1,4 @@ |
|||
package com.yunqiinnovation.ble_service |
|||
package com.yunqiinnovation.agent_service |
|||
|
|||
import android.content.* |
|||
import android.os.Build |
|||
@ -1,4 +1,4 @@ |
|||
package com.yunqiinnovation.ble_service |
|||
package com.yunqiinnovation.agent_service |
|||
|
|||
import android.content.Context |
|||
import androidx.security.crypto.EncryptedSharedPreferences |
|||
@ -0,0 +1,994 @@ |
|||
import Foundation |
|||
import MicrosoftCognitiveServicesSpeech |
|||
import open_ai_service |
|||
import chat_storage |
|||
|
|||
/// 代理服务 - 处理语音识别、TTS和AI对话相关逻辑 |
|||
/// |
|||
/// 负责集成Azure语音服务、OpenAI服务和本地存储服务, |
|||
/// 提供语音识别、语音合成、AI对话等功能 |
|||
class AgentService { |
|||
// 常量定义 |
|||
private let TAG = "AgentService" |
|||
|
|||
// 上下文和监听器 |
|||
private var listeners = NSHashTable<AnyObject>.weakObjects() |
|||
|
|||
// 配置参数 |
|||
private var azureSpeechKey: String = "" |
|||
private var azureSpeechRegion: String = "" |
|||
private var openaiApiKey: String = "" |
|||
private var openaiBaseUrl: String = "" |
|||
private var openaiModel: String = "gpt-3.5-turbo" |
|||
private var mcpServer: String = "" |
|||
|
|||
// Azure服务 |
|||
private var azureAsrHelper: AzureAsrHelper? |
|||
private var azureTtsHelper: AzureTtsHelper? |
|||
|
|||
// OpenAI服务 |
|||
private var openAIService: OpenAIServiceBridge? |
|||
|
|||
// 聊天存储服务 |
|||
private var chatStorageHelper: ChatStorageHelperBridge? |
|||
|
|||
// 会话ID,用于区分不同聊天上下文 |
|||
private let sessionId = "default_agent" |
|||
|
|||
// 历史聊天消息缓存 |
|||
private var historyMessages = [[String: Any]]() |
|||
|
|||
// 系统提示词 |
|||
private var systemPrompt = "" |
|||
|
|||
// 状态 |
|||
private var isInitialized = false |
|||
private var isRecognitionActive = false |
|||
private var isTtsSpeaking = false |
|||
private var hasSpeechDetected = false |
|||
private var isAiStreaming = false |
|||
|
|||
// AI流生成相关 |
|||
private var currentAiTask: DispatchWorkItem? |
|||
|
|||
// 空闲检测相关 |
|||
private var idleCheckTimer: Timer? |
|||
private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数 |
|||
|
|||
/// 初始化 |
|||
init() { |
|||
// 创建OpenAI服务桥接器 |
|||
openAIService = OpenAIServiceBridge() |
|||
|
|||
// 创建聊天存储桥接器 |
|||
chatStorageHelper = ChatStorageHelperBridge() |
|||
} |
|||
|
|||
/** |
|||
* 初始化系统提示词 |
|||
*/ |
|||
private func initSystemPrompt() { |
|||
systemPrompt = """ |
|||
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。 |
|||
遵循以下指导原则: |
|||
1. 保持简短精炼的回答,因为用户是通过语音与你交流 |
|||
2. 优先使用中文回复,除非用户明确要求使用其他语言 |
|||
3. 当用户问题不明确时,礼貌地请求更多信息 |
|||
4. 避免过长的列表,尽量将信息分成小段 |
|||
5. 不要使用需要视觉展示的元素(如表格、图表或代码块) |
|||
6. 记住用户之前的对话内容,保持对话连贯 |
|||
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 |
|||
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 |
|||
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 |
|||
""" |
|||
} |
|||
|
|||
/** |
|||
* 初始化 |
|||
* |
|||
* - Parameter config: 配置参数,包含所需的所有API密钥和设置 |
|||
* - Returns: 是否初始化成功 |
|||
*/ |
|||
func initialize(config: [String: Any]) -> Bool { |
|||
if isInitialized { return true } |
|||
|
|||
do { |
|||
// 从配置中获取参数 |
|||
if let azureKey = config["azureSpeechKey"] as? String { |
|||
azureSpeechKey = azureKey |
|||
} |
|||
|
|||
if let azureRegion = config["azureSpeechRegion"] as? String { |
|||
azureSpeechRegion = azureRegion |
|||
} |
|||
|
|||
if let openaiKey = config["openaiApiKey"] as? String { |
|||
openaiApiKey = openaiKey |
|||
} |
|||
|
|||
if let baseUrl = config["openaiBaseUrl"] as? String { |
|||
openaiBaseUrl = baseUrl |
|||
} |
|||
|
|||
if let model = config["openaiModel"] as? String { |
|||
openaiModel = model |
|||
} |
|||
|
|||
if let server = config["mcpServer"] as? String { |
|||
mcpServer = server |
|||
} |
|||
|
|||
// 自定义系统提示词 |
|||
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { |
|||
systemPrompt = customSystemPrompt |
|||
} else { |
|||
// 使用默认系统提示词 |
|||
initSystemPrompt() |
|||
} |
|||
|
|||
// 检查必要参数 |
|||
if azureSpeechKey.isEmpty || azureSpeechRegion.isEmpty || openaiApiKey.isEmpty { |
|||
print("\(TAG): 初始化失败:关键配置参数缺失") |
|||
return false |
|||
} |
|||
|
|||
// 初始化OpenAI服务 |
|||
openAIService?.initialize(apiKey: openaiApiKey, baseUrl: openaiBaseUrl, model: openaiModel, mcpServer: mcpServer) |
|||
|
|||
// 初始化Azure ASR |
|||
azureAsrHelper = AzureAsrHelper(self) |
|||
let asrInitResult = azureAsrHelper?.initialize( |
|||
subscriptionKey: azureSpeechKey, |
|||
region: azureSpeechRegion, |
|||
audioSourceType: .microphone |
|||
) ?? false |
|||
|
|||
if !asrInitResult { |
|||
print("\(TAG): Azure ASR初始化失败") |
|||
return false |
|||
} |
|||
|
|||
// 初始化Azure TTS |
|||
azureTtsHelper = AzureTtsHelper(self) |
|||
let ttsInitResult = azureTtsHelper?.initialize( |
|||
subscriptionKey: azureSpeechKey, |
|||
region: azureSpeechRegion, |
|||
callback: self |
|||
) ?? false |
|||
|
|||
if !ttsInitResult { |
|||
print("\(TAG): Azure TTS初始化失败") |
|||
return false |
|||
} |
|||
|
|||
// 加载最近的聊天记录 |
|||
loadChatHistory() |
|||
|
|||
isInitialized = true |
|||
print("\(TAG): 代理服务初始化成功") |
|||
return true |
|||
} catch { |
|||
print("\(TAG): 初始化失败:\(error.localizedDescription)") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 添加事件监听器 |
|||
* |
|||
* - Parameter listener: 要添加的监听器 |
|||
*/ |
|||
func addListener(_ listener: AgentServiceListener) { |
|||
listeners.add(listener as AnyObject) |
|||
} |
|||
|
|||
/** |
|||
* 设置事件监听器(替换现有监听器) |
|||
* |
|||
* - Parameter listener: 要设置的监听器 |
|||
*/ |
|||
func setListener(_ listener: AgentServiceListener) { |
|||
listeners.removeAllObjects() |
|||
listeners.add(listener as AnyObject) |
|||
} |
|||
|
|||
/** |
|||
* 移除事件监听器 |
|||
* |
|||
* - Parameter listener: 要移除的监听器 |
|||
*/ |
|||
func removeListener(_ listener: AgentServiceListener) { |
|||
listeners.remove(listener as AnyObject) |
|||
} |
|||
|
|||
/** |
|||
* 移除所有事件监听器 |
|||
*/ |
|||
func clearListeners() { |
|||
listeners.removeAllObjects() |
|||
} |
|||
|
|||
/** |
|||
* 启动空闲检测 |
|||
*/ |
|||
private func startIdleCheck() { |
|||
stopIdleCheck() // 先停止现有的检查 |
|||
|
|||
if !isRecognitionActive { return } |
|||
|
|||
// 创建定时器 |
|||
idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [weak self] _ in |
|||
guard let self = self else { return } |
|||
|
|||
// 如果状态仍然是空闲的,则停止识别 |
|||
if self.isRecognitionActive && !self.hasSpeechDetected && !self.isTtsSpeaking && !self.isAiStreaming { |
|||
self.stopRecognition() |
|||
self.sendEvent("auto_stop", ["reason": "idle_timeout", "seconds": self.maxIdleSeconds]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 停止空闲检测 |
|||
*/ |
|||
private func stopIdleCheck() { |
|||
idleCheckTimer?.invalidate() |
|||
idleCheckTimer = nil |
|||
} |
|||
|
|||
/** |
|||
* 重启空闲检测 |
|||
* 当状态发生变化时调用 |
|||
*/ |
|||
private func restartIdleCheck() { |
|||
if isRecognitionActive { |
|||
startIdleCheck() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 开始语音识别 |
|||
* |
|||
* - Returns: 是否成功开始识别 |
|||
*/ |
|||
func startRecognition() -> Bool { |
|||
if !isInitialized { |
|||
print("\(TAG): 服务未初始化") |
|||
return false |
|||
} |
|||
|
|||
if isRecognitionActive { return true } |
|||
|
|||
isRecognitionActive = true |
|||
hasSpeechDetected = false |
|||
|
|||
do { |
|||
// 创建连续识别回调 |
|||
class ContinuousRecognizeCallbackImpl: AzureAsrHelper.ContinuousRecognizeCallback { |
|||
private weak var service: AgentService? |
|||
|
|||
init(_ service: AgentService) { |
|||
self.service = service |
|||
} |
|||
|
|||
func onRecognizing(recognizing: String, detectedLanguage: String) { |
|||
guard let service = service else { return } |
|||
|
|||
if !recognizing.isEmpty { |
|||
// 检测到语音,更新状态 |
|||
let previousHasSpeech = service.hasSpeechDetected |
|||
service.hasSpeechDetected = true |
|||
|
|||
// 状态发生变化时重启空闲检测 |
|||
if !previousHasSpeech { |
|||
service.restartIdleCheck() |
|||
} |
|||
|
|||
service.sendEvent("recognizing", [ |
|||
"text": recognizing, |
|||
"language": detectedLanguage |
|||
]) |
|||
|
|||
// 如果TTS正在播放或AI正在生成,则触发打断 |
|||
if service.isTtsSpeaking || service.isAiStreaming { |
|||
service.interruptCurrentResponse() |
|||
} |
|||
} |
|||
} |
|||
|
|||
func onResult(text: String, detectedLanguage: String) { |
|||
guard let service = service else { return } |
|||
|
|||
if !text.isEmpty { |
|||
service.sendEvent("recognition_result", [ |
|||
"text": text, |
|||
"language": detectedLanguage |
|||
]) |
|||
|
|||
service.processWithOpenAI(text: text) |
|||
} |
|||
|
|||
// 重置状态,继续识别 |
|||
let previousHasSpeech = service.hasSpeechDetected |
|||
service.hasSpeechDetected = false |
|||
|
|||
// 状态发生变化时重启空闲检测 |
|||
if previousHasSpeech { |
|||
service.restartIdleCheck() |
|||
} |
|||
} |
|||
|
|||
func onSessionStarted() { |
|||
guard let service = service else { return } |
|||
|
|||
service.sendEvent("recognition_started", ["status": "started"]) |
|||
// 启动空闲检测 |
|||
service.startIdleCheck() |
|||
} |
|||
|
|||
func onSessionStopped() { |
|||
guard let service = service else { return } |
|||
|
|||
service.isRecognitionActive = false |
|||
service.stopIdleCheck() |
|||
service.sendEvent("recognition_stopped", ["status": "stopped"]) |
|||
} |
|||
|
|||
func onCanceled(reason: String, errorDetails: String) { |
|||
guard let service = service else { return } |
|||
|
|||
service.isRecognitionActive = false |
|||
service.stopIdleCheck() |
|||
service.sendEvent("recognition_canceled", [ |
|||
"reason": reason, |
|||
"details": errorDetails |
|||
]) |
|||
} |
|||
|
|||
func onError(error: String) { |
|||
guard let service = service else { return } |
|||
|
|||
service.isRecognitionActive = false |
|||
service.stopIdleCheck() |
|||
print("\(service.TAG): 语音识别出错: \(error)") |
|||
service.sendEvent("error", [ |
|||
"code": "RECOGNITION_ERROR", |
|||
"message": error |
|||
]) |
|||
} |
|||
} |
|||
|
|||
let callback = ContinuousRecognizeCallbackImpl(self) |
|||
let result = azureAsrHelper?.startContinuousRecognition(callback) ?? false |
|||
|
|||
if !result { |
|||
isRecognitionActive = false |
|||
print("\(TAG): 启动语音识别失败") |
|||
sendEvent("error", [ |
|||
"code": "RECOGNITION_START_ERROR", |
|||
"message": "启动语音识别失败" |
|||
]) |
|||
} |
|||
|
|||
return result |
|||
} catch { |
|||
isRecognitionActive = false |
|||
print("\(TAG): 启动语音识别失败: \(error.localizedDescription)") |
|||
sendEvent("error", [ |
|||
"code": "RECOGNITION_START_ERROR", |
|||
"message": error.localizedDescription |
|||
]) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 停止语音识别 |
|||
*/ |
|||
func stopRecognition() { |
|||
if !isRecognitionActive { return } |
|||
|
|||
print("\(TAG): 停止语音识别") |
|||
|
|||
// 停止识别 |
|||
let _ = azureAsrHelper?.stopContinuousRecognition() |
|||
isRecognitionActive = false |
|||
stopIdleCheck() |
|||
print("\(TAG): 语音识别已停止") |
|||
} |
|||
|
|||
/** |
|||
* 打断当前响应 |
|||
* 停止TTS播放和AI流输出 |
|||
*/ |
|||
func interruptCurrentResponse() { |
|||
if isAiStreaming || isTtsSpeaking { |
|||
// 停止TTS播放 |
|||
stopTts() |
|||
|
|||
// 停止AI流输出 |
|||
stopAiStream() |
|||
|
|||
// 发送打断事件 |
|||
sendEvent("response_interrupted", ["status": "interrupted"]) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 停止AI流输出 |
|||
*/ |
|||
private func stopAiStream() { |
|||
if isAiStreaming { |
|||
// 取消当前AI生成任务 |
|||
currentAiTask?.cancel() |
|||
currentAiTask = nil |
|||
|
|||
// 通知OpenAI服务终止当前流式请求 |
|||
openAIService?.cancelCurrentStream() |
|||
|
|||
// 更新状态 |
|||
isAiStreaming = false |
|||
|
|||
// 记录日志 |
|||
print("\(TAG): AI流输出已停止") |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理文本输入 |
|||
* 作为语音输入的补充,直接处理文本并通过事件返回结果 |
|||
* |
|||
* - Parameters: |
|||
* - text: 用户输入文本 |
|||
* - speakResponse: 是否朗读回复,默认为false |
|||
* - Returns: 是否成功开始处理 |
|||
*/ |
|||
func processTextInput(text: String, speakResponse: Bool = false) -> Bool { |
|||
if !isInitialized { |
|||
print("\(TAG): 服务未初始化") |
|||
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"]) |
|||
return false |
|||
} |
|||
|
|||
if text.isEmpty { |
|||
print("\(TAG): 文本输入不能为空") |
|||
sendEvent("error", ["code": "EMPTY_TEXT", "message": "文本输入不能为空"]) |
|||
return false |
|||
} |
|||
|
|||
// 使用OpenAI处理文本 |
|||
processWithOpenAI(text: text, speakResponse: speakResponse) |
|||
return true |
|||
} |
|||
|
|||
/** |
|||
* 使用OpenAI处理语音识别结果 |
|||
* |
|||
* - Parameter text: 用户输入文本 |
|||
*/ |
|||
private func processWithOpenAI(text: String) { |
|||
processWithOpenAI(text: text, speakResponse: true) |
|||
} |
|||
|
|||
/** |
|||
* 使用OpenAI处理文本消息 |
|||
* |
|||
* - Parameters: |
|||
* - text: 用户输入文本 |
|||
* - speakResponse: 是否使用TTS朗读回复 |
|||
*/ |
|||
private func processWithOpenAI(text: String, speakResponse: Bool = true) { |
|||
print("\(TAG): 用户问题: \(text)") |
|||
|
|||
// 创建用户文本消息并处理 |
|||
guard let userMessage = openAIService?.createUserMessage(text: text) else { |
|||
print("\(TAG): 创建用户消息失败") |
|||
return |
|||
} |
|||
|
|||
processWithOpenAIInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) |
|||
} |
|||
|
|||
/** |
|||
* 使用OpenAI处理图片 |
|||
* |
|||
* - Parameters: |
|||
* - imagePath: 图片文件路径 |
|||
* - text: 可选的文本描述或问题 |
|||
* - speakResponse: 是否朗读回复 |
|||
* - Returns: 是否成功开始处理 |
|||
*/ |
|||
func processImageInput(imagePath: String, text: String = "", speakResponse: Bool = false) -> Bool { |
|||
if !isInitialized { |
|||
print("\(TAG): 服务未初始化") |
|||
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"]) |
|||
return false |
|||
} |
|||
|
|||
if imagePath.isEmpty { |
|||
print("\(TAG): 图片路径不能为空") |
|||
sendEvent("error", ["code": "EMPTY_IMAGE_PATH", "message": "图片路径不能为空"]) |
|||
return false |
|||
} |
|||
|
|||
// 通知开始处理图片 |
|||
sendEvent("image_processing", [ |
|||
"status": "processing", |
|||
"imagePath": imagePath |
|||
]) |
|||
|
|||
// 异步处理图片 |
|||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
// 将图片转换为Base64格式 |
|||
guard let imageBase64 = self.openAIService?.fileToBase64(filePath: imagePath) else { |
|||
DispatchQueue.main.async { |
|||
print("\(self.TAG): 图片转换失败: \(imagePath)") |
|||
self.sendEvent("error", [ |
|||
"code": "IMAGE_CONVERSION_FAILED", |
|||
"message": "图片转换失败" |
|||
]) |
|||
} |
|||
return |
|||
} |
|||
|
|||
// 通知图片准备完成 |
|||
DispatchQueue.main.async { |
|||
self.sendEvent("image_ready", [ |
|||
"status": "ready", |
|||
"imagePath": imagePath |
|||
]) |
|||
|
|||
// 处理包含图片的消息 |
|||
self.processImageWithOpenAI(imageBase64: imageBase64, text: text, speakResponse: speakResponse) |
|||
} |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
/** |
|||
* 使用OpenAI处理图片 |
|||
* |
|||
* - Parameters: |
|||
* - imageBase64: Base64编码的图片数据 |
|||
* - text: 可选的文本描述或问题 |
|||
* - speakResponse: 是否朗读回复 |
|||
*/ |
|||
private func processImageWithOpenAI(imageBase64: String, text: String = "", speakResponse: Bool = false) { |
|||
print("\(TAG): 处理图片输入: \(text.isEmpty ? "无附加文本" : "附带文本: \(text)")") |
|||
|
|||
// 创建带图片的用户消息并处理 |
|||
guard let userMessage = openAIService?.createUserMessageWithImage(text: text, imageBase64: imageBase64) else { |
|||
print("\(TAG): 创建带图片的用户消息失败") |
|||
return |
|||
} |
|||
|
|||
// 图片描述用于存储 |
|||
let displayText = text.isEmpty ? "[图片]" : text |
|||
processWithOpenAIInternal(userMessage: userMessage, displayText: displayText, speakResponse: speakResponse, hasImage: true) |
|||
} |
|||
|
|||
/** |
|||
* 内部方法:通用的OpenAI处理逻辑 |
|||
* |
|||
* - Parameters: |
|||
* - userMessage: 用户消息(可以是文本或图片格式) |
|||
* - displayText: 用于显示和存储的文本 |
|||
* - speakResponse: 是否朗读回复 |
|||
* - hasImage: 是否包含图片 |
|||
*/ |
|||
private func processWithOpenAIInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) { |
|||
// 如果有正在进行的AI流式输出,先停止它 |
|||
stopAiStream() |
|||
|
|||
// 创建AI任务 |
|||
let workItem = DispatchWorkItem { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
// 设置状态为正在流式输出 |
|||
self.isAiStreaming = true |
|||
|
|||
// 使用历史记录作为上下文发送到OpenAI |
|||
var responseBuilder = "" |
|||
|
|||
// 添加系统提示词到历史消息的副本中 |
|||
var messagesWithSystemPrompt: [[String: Any]] = [] |
|||
|
|||
// 先添加系统提示词 |
|||
if !self.systemPrompt.isEmpty { |
|||
if let systemMessage = self.openAIService?.createSystemMessage(text: self.systemPrompt) { |
|||
messagesWithSystemPrompt.append(systemMessage) |
|||
} |
|||
} |
|||
|
|||
// 再添加历史消息 |
|||
messagesWithSystemPrompt.append(contentsOf: self.historyMessages) |
|||
messagesWithSystemPrompt.append(userMessage) |
|||
|
|||
// 将用户消息添加到历史记录 |
|||
self.addToHistoryMessages(userMessage) |
|||
|
|||
// 流式回调 |
|||
class StreamCallbackBridge: NSObject, OpenAIStreamCallback { |
|||
private weak var service: AgentService? |
|||
private var responseBuilder: String |
|||
private let speakResponse: Bool |
|||
private let displayText: String |
|||
private let hasImage: Bool |
|||
|
|||
init(_ service: AgentService, responseBuilder: String = "", speakResponse: Bool, displayText: String, hasImage: Bool) { |
|||
self.service = service |
|||
self.responseBuilder = responseBuilder |
|||
self.speakResponse = speakResponse |
|||
self.displayText = displayText |
|||
self.hasImage = hasImage |
|||
super.init() |
|||
} |
|||
|
|||
func onToken(token: String) { |
|||
guard let service = service else { return } |
|||
|
|||
responseBuilder.append(token) |
|||
if speakResponse { |
|||
_ = service.azureTtsHelper?.speakStream(token) |
|||
} |
|||
// 发送流式回复token |
|||
service.sendEvent("assistant_token", ["token": token]) |
|||
} |
|||
|
|||
func onComplete() { |
|||
guard let service = service else { return } |
|||
|
|||
// 视情况决定是否朗读回复 |
|||
if speakResponse { |
|||
_ = service.azureTtsHelper?.flushStream() |
|||
} |
|||
|
|||
if !responseBuilder.isEmpty { |
|||
// 发送完整回复,包含是否有图片的标记 |
|||
var responseData: [String: Any] = [ |
|||
"text": responseBuilder, |
|||
"userInput": displayText |
|||
] |
|||
|
|||
if hasImage { |
|||
responseData["hasImage"] = true |
|||
} |
|||
|
|||
service.sendEvent("assistant_response", responseData) |
|||
|
|||
// 添加AI回复到历史记录 |
|||
if let assistantMessage = service.openAIService?.createAssistantMessage(text: responseBuilder) { |
|||
service.addToHistoryMessages(assistantMessage) |
|||
} |
|||
|
|||
// 保存聊天记录 |
|||
service.saveChatMessage(userMessage: displayText, assistantMessage: responseBuilder) |
|||
} |
|||
|
|||
// 标记AI流式输出已完成 |
|||
service.isAiStreaming = false |
|||
service.currentAiTask = nil |
|||
} |
|||
|
|||
func onError(error: Error) { |
|||
guard let service = service else { return } |
|||
|
|||
print("\(service.TAG): AI处理出错: \(error.localizedDescription)") |
|||
service.sendEvent("error", [ |
|||
"code": "AI_ERROR", |
|||
"message": error.localizedDescription |
|||
]) |
|||
|
|||
// 标记AI流式输出已完成 |
|||
service.isAiStreaming = false |
|||
service.currentAiTask = nil |
|||
} |
|||
|
|||
func onFunctionCall(call: [String: Any]) { |
|||
guard let service = service else { return } |
|||
|
|||
if let name = call["name"] as? String { |
|||
service.sendEvent("function_call", [ |
|||
"name": name, |
|||
"arguments": call |
|||
]) |
|||
|
|||
if name == "exit_interaction" { |
|||
service.stopRecognition() |
|||
} |
|||
} |
|||
} |
|||
|
|||
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) { |
|||
guard let service = service else { return } |
|||
|
|||
service.sendEvent("function_call_result", [ |
|||
"function_call": functionCall, |
|||
"result": functionCallResult, |
|||
]) |
|||
} |
|||
} |
|||
|
|||
let callback = StreamCallbackBridge(self, responseBuilder: responseBuilder, speakResponse: speakResponse, displayText: displayText, hasImage: hasImage) |
|||
|
|||
self.openAIService?.sendMessageStream(messages: messagesWithSystemPrompt, callback: callback) |
|||
} |
|||
|
|||
// 保存任务引用并在全局队列中执行 |
|||
currentAiTask = workItem |
|||
DispatchQueue.global(qos: .userInitiated).async(execute: workItem) |
|||
} |
|||
|
|||
/** |
|||
* 加载最近的聊天历史记录 |
|||
*/ |
|||
private func loadChatHistory() { |
|||
// 清空当前历史记录 |
|||
historyMessages.removeAll() |
|||
|
|||
// 使用ChatStorageHelper直接获取最近消息 |
|||
guard let recentMessages = chatStorageHelper?.getRecentMessages(sessionId: sessionId, count: 10) else { |
|||
print("\(TAG): 没有找到历史记录") |
|||
return |
|||
} |
|||
|
|||
// 将消息添加到历史记录 |
|||
for message in recentMessages { |
|||
if let sender = message["sender"] as? String, |
|||
let content = message["message"] as? String { |
|||
|
|||
if sender == "user" { |
|||
if let userMessage = openAIService?.createUserMessage(text: content) { |
|||
addToHistoryMessages(userMessage) |
|||
} |
|||
} else if sender == "assistant" { |
|||
if let assistantMessage = openAIService?.createAssistantMessage(text: content) { |
|||
addToHistoryMessages(assistantMessage) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
print("\(TAG): 已加载\(recentMessages.count)条历史记录") |
|||
} |
|||
|
|||
/** |
|||
* 添加消息到历史记录,保持最近10条 |
|||
*/ |
|||
private func addToHistoryMessages(_ message: [String: Any]) { |
|||
// 添加新消息 |
|||
historyMessages.append(message) |
|||
|
|||
// 如果超过10条,删除最早的消息 |
|||
while historyMessages.count > 10 { |
|||
historyMessages.removeFirst() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* TTS播放函数 |
|||
* |
|||
* - Parameter text: 要播放的文本 |
|||
*/ |
|||
func speakText(text: String) { |
|||
if text.isEmpty { return } |
|||
|
|||
// 更新状态 |
|||
isTtsSpeaking = true |
|||
restartIdleCheck() // 状态变化,重启检测 |
|||
|
|||
// 播放文本 |
|||
_ = azureTtsHelper?.speakText(text) |
|||
} |
|||
|
|||
/** |
|||
* 停止TTS播放 |
|||
*/ |
|||
func stopTts() { |
|||
if isTtsSpeaking { |
|||
_ = azureTtsHelper?.stopSpeaking() |
|||
isTtsSpeaking = false |
|||
restartIdleCheck() // 状态变化,重启检测 |
|||
sendEvent("tts_stopped", ["status": "stopped"]) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 保存聊天消息 |
|||
*/ |
|||
private func saveChatMessage(userMessage: String, assistantMessage: String) { |
|||
DispatchQueue.global(qos: .background).async { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
// 保存用户消息 |
|||
let userMessageId = self.chatStorageHelper?.saveMessage( |
|||
sessionId: self.sessionId, |
|||
message: userMessage, |
|||
sender: "user" |
|||
) ?? -1 |
|||
|
|||
if userMessageId != -1 { |
|||
// 保存AI回复 |
|||
let assistantMessageId = self.chatStorageHelper?.saveMessage( |
|||
sessionId: self.sessionId, |
|||
message: assistantMessage, |
|||
sender: "assistant" |
|||
) ?? -1 |
|||
|
|||
if assistantMessageId == -1 { |
|||
print("\(self.TAG): 保存助手消息失败") |
|||
} |
|||
} else { |
|||
print("\(self.TAG): 保存用户消息失败") |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清除聊天历史 |
|||
*/ |
|||
func clearChatHistory(completion: @escaping (Bool) -> Void) { |
|||
DispatchQueue.global(qos: .background).async { [weak self] in |
|||
guard let self = self else { |
|||
DispatchQueue.main.async { |
|||
completion(false) |
|||
} |
|||
return |
|||
} |
|||
|
|||
// 清除指定会话的所有消息 |
|||
let success = self.chatStorageHelper?.deleteMessages(sessionId: self.sessionId) ?? false |
|||
|
|||
if success { |
|||
// 清空内存中的历史记录 |
|||
self.historyMessages.removeAll() |
|||
print("\(self.TAG): 聊天历史已清除") |
|||
} else { |
|||
print("\(self.TAG): 清除聊天历史失败") |
|||
} |
|||
|
|||
DispatchQueue.main.async { |
|||
completion(success) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 发送事件 |
|||
*/ |
|||
private func sendEvent(_ eventName: String, _ data: [String: Any]) { |
|||
// 向所有监听器发送事件 |
|||
let allListeners = listeners.allObjects |
|||
for case let listener as AgentServiceListener in allListeners { |
|||
listener.onEvent(eventName: eventName, data: data) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 释放资源 |
|||
*/ |
|||
func dispose() { |
|||
stopRecognition() |
|||
stopTts() |
|||
stopAiStream() |
|||
stopIdleCheck() |
|||
|
|||
azureAsrHelper?.dispose() |
|||
azureTtsHelper?.dispose() |
|||
|
|||
// 清除所有监听器 |
|||
clearListeners() |
|||
|
|||
// 重置单例状态,以便下次使用时可以重新初始化 |
|||
isInitialized = false |
|||
|
|||
print("\(TAG): 代理服务资源已释放") |
|||
} |
|||
} |
|||
|
|||
// MARK: - AzureTtsHelper.TtsCallback |
|||
extension AgentService: AzureTtsHelper.TtsCallback { |
|||
func onSynthesisStarted() { |
|||
isTtsSpeaking = true |
|||
// 状态变化,重置空闲检测 |
|||
restartIdleCheck() |
|||
sendEvent("tts_started", ["status": "started"]) |
|||
} |
|||
|
|||
func onSynthesizing() { |
|||
// 可以在此添加TTS合成中的处理逻辑 |
|||
} |
|||
|
|||
func onSynthesisCompleted() { |
|||
isTtsSpeaking = false |
|||
// 状态变化,重启空闲检测 |
|||
restartIdleCheck() |
|||
sendEvent("tts_completed", ["status": "completed"]) |
|||
} |
|||
|
|||
func onSynthesisCanceled() { |
|||
isTtsSpeaking = false |
|||
// 状态变化,重启空闲检测 |
|||
restartIdleCheck() |
|||
sendEvent("tts_canceled", ["status": "canceled"]) |
|||
} |
|||
} |
|||
|
|||
// MARK: - 外部插件桥接器 |
|||
|
|||
/// 桥接OpenAIService插件 |
|||
class OpenAIServiceBridge { |
|||
private let service = OpenAIService() |
|||
|
|||
/// 初始化服务 |
|||
func initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String) { |
|||
service.initialize(apiKey: apiKey, baseUrl: baseUrl, model: model, mcpServer: mcpServer) |
|||
} |
|||
|
|||
/// 创建系统消息 |
|||
func createSystemMessage(text: String) -> [String: Any] { |
|||
return service.createSystemMessage(text) |
|||
} |
|||
|
|||
/// 创建用户消息 |
|||
func createUserMessage(text: String) -> [String: Any] { |
|||
return service.createUserMessage(text) |
|||
} |
|||
|
|||
/// 创建助手消息 |
|||
func createAssistantMessage(text: String) -> [String: Any] { |
|||
return service.createAssistantMessage(text) |
|||
} |
|||
|
|||
/// 创建带图片的用户消息 |
|||
func createUserMessageWithImage(text: String, imageBase64: String) -> [String: Any]? { |
|||
return service.createUserMessageWithImage(text: text, imageBase64: imageBase64) |
|||
} |
|||
|
|||
/// 文件转Base64 |
|||
func fileToBase64(filePath: String) -> String? { |
|||
return service.fileToBase64(filePath: filePath) |
|||
} |
|||
|
|||
/// 发送流式消息 |
|||
func sendMessageStream(messages: [[String: Any]], callback: OpenAIStreamCallback) { |
|||
service.sendMessageStream(messages: messages, callback: callback) |
|||
} |
|||
|
|||
/// 取消当前流 |
|||
func cancelCurrentStream() { |
|||
service.cancelCurrentStream() |
|||
} |
|||
} |
|||
|
|||
/// OpenAI流式回调协议 |
|||
@objc protocol OpenAIStreamCallback { |
|||
func onToken(token: String) |
|||
func onComplete() |
|||
func onError(error: Error) |
|||
func onFunctionCall(call: [String: Any]) |
|||
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) |
|||
} |
|||
|
|||
/// 桥接ChatStorageHelper插件 |
|||
class ChatStorageHelperBridge { |
|||
private let helper = ChatStorageHelper() |
|||
|
|||
/// 保存消息 |
|||
func saveMessage(sessionId: String, message: String, sender: String) -> Int64 { |
|||
return helper.saveMessage(sessionId: sessionId, message: message, sender: sender) |
|||
} |
|||
|
|||
/// 获取最近消息 |
|||
func getRecentMessages(sessionId: String, count: Int) -> [[String: Any]]? { |
|||
return helper.getRecentMessages(sessionId: sessionId, count: count) |
|||
} |
|||
|
|||
/// 删除消息 |
|||
func deleteMessages(sessionId: String) -> Bool { |
|||
return helper.deleteMessages(sessionId: sessionId) |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
import Foundation |
|||
|
|||
/// 代理服务事件监听器协议 |
|||
/// 用于接收代理服务发送的各类事件 |
|||
protocol AgentServiceListener: AnyObject { |
|||
/// 事件回调 |
|||
/// - Parameters: |
|||
/// - eventName: 事件名称 |
|||
/// - data: 事件数据 |
|||
func onEvent(eventName: String, data: [String: Any]) |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
#import <Flutter/Flutter.h> |
|||
|
|||
@interface AgentServicePlugin : NSObject<FlutterPlugin> |
|||
@end |
|||
@ -0,0 +1,12 @@ |
|||
#import "AgentServicePlugin.h" |
|||
#if __has_include(<agent_service/agent_service-Swift.h>) |
|||
#import <agent_service/agent_service-Swift.h> |
|||
#else |
|||
#import "agent_service-Swift.h" |
|||
#endif |
|||
|
|||
@implementation AgentServicePlugin |
|||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
|||
[SwiftAgentServicePlugin registerWithRegistrar:registrar]; |
|||
} |
|||
@end |
|||
@ -0,0 +1,259 @@ |
|||
import Flutter |
|||
import UIKit |
|||
import open_ai_service |
|||
import chat_storage |
|||
|
|||
/// Swift版Agent Service插件 |
|||
/// 处理Flutter与原生层的通信 |
|||
public class SwiftAgentServicePlugin: NSObject, FlutterPlugin { |
|||
// Flutter方法通道 |
|||
private var methodChannel: FlutterMethodChannel? |
|||
|
|||
// Flutter事件通道 |
|||
private var eventChannel: FlutterEventChannel? |
|||
|
|||
// 事件sink,用于向Flutter发送事件 |
|||
private var eventSink: FlutterEventSink? |
|||
|
|||
// 核心服务实例 |
|||
private var agentService: AgentService? |
|||
|
|||
/// 注册插件 |
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
let instance = SwiftAgentServicePlugin() |
|||
|
|||
// 初始化方法通道 |
|||
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger()) |
|||
registrar.addMethodCallDelegate(instance, channel: methodChannel) |
|||
instance.methodChannel = methodChannel |
|||
|
|||
// 初始化事件通道 |
|||
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger()) |
|||
eventChannel.setStreamHandler(instance) |
|||
instance.eventChannel = eventChannel |
|||
} |
|||
|
|||
/// 处理方法调用 |
|||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
switch call.method { |
|||
case "initialize": |
|||
handleInitialize(call, result) |
|||
|
|||
case "startConversation": |
|||
handleStartConversation(result) |
|||
|
|||
case "stopConversation": |
|||
handleStopConversation(result) |
|||
|
|||
case "processTextInput": |
|||
handleProcessTextInput(call, result) |
|||
|
|||
case "speakText": |
|||
handleSpeakText(call, result) |
|||
|
|||
case "stopTts": |
|||
handleStopTts(result) |
|||
|
|||
case "clearChatHistory": |
|||
handleClearChatHistory(result) |
|||
|
|||
case "interruptCurrentResponse": |
|||
handleInterruptCurrentResponse(result) |
|||
|
|||
case "processImageInput": |
|||
handleProcessImageInput(call, result) |
|||
|
|||
case "dispose": |
|||
handleDispose(result) |
|||
|
|||
default: |
|||
result(FlutterMethodNotImplemented) |
|||
} |
|||
} |
|||
|
|||
// MARK: - 方法处理 |
|||
|
|||
/// 初始化 |
|||
private func handleInitialize(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|||
guard let args = call.arguments as? [String: Any], |
|||
let config = args["config"] as? [String: Any] else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "缺少配置参数", details: nil)) |
|||
return |
|||
} |
|||
|
|||
// 初始化代理服务 |
|||
if agentService == nil { |
|||
agentService = AgentService() |
|||
|
|||
// 设置事件监听器 |
|||
agentService?.setListener(self) |
|||
} |
|||
|
|||
// 执行初始化 |
|||
let success = agentService?.initialize(config: config) ?? false |
|||
result(success) |
|||
} |
|||
|
|||
/// 开始对话 |
|||
private func handleStartConversation(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
let success = service.startRecognition() |
|||
result(success) |
|||
} |
|||
|
|||
/// 停止对话 |
|||
private func handleStopConversation(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
service.stopRecognition() |
|||
result(true) |
|||
} |
|||
|
|||
/// 处理文本输入 |
|||
private func handleProcessTextInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
guard let args = call.arguments as? [String: Any], |
|||
let text = args["text"] as? String else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil)) |
|||
return |
|||
} |
|||
|
|||
let speakResponse = (args["speakResponse"] as? Bool) ?? false |
|||
let success = service.processTextInput(text: text, speakResponse: speakResponse) |
|||
result(success) |
|||
} |
|||
|
|||
/// 朗读文本 |
|||
private func handleSpeakText(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
guard let args = call.arguments as? [String: Any], |
|||
let text = args["text"] as? String else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil)) |
|||
return |
|||
} |
|||
|
|||
service.speakText(text: text) |
|||
result(true) |
|||
} |
|||
|
|||
/// 停止语音合成 |
|||
private func handleStopTts(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
service.stopTts() |
|||
result(true) |
|||
} |
|||
|
|||
/// 清除聊天历史 |
|||
private func handleClearChatHistory(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
service.clearChatHistory { success in |
|||
result(success) |
|||
} |
|||
} |
|||
|
|||
/// 中断当前响应 |
|||
private func handleInterruptCurrentResponse(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
service.interruptCurrentResponse() |
|||
result(true) |
|||
} |
|||
|
|||
/// 处理图片输入 |
|||
private func handleProcessImageInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
guard let args = call.arguments as? [String: Any], |
|||
let imagePath = args["imagePath"] as? String else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的图片路径参数", details: nil)) |
|||
return |
|||
} |
|||
|
|||
let text = (args["text"] as? String) ?? "" |
|||
let speakResponse = (args["speakResponse"] as? Bool) ?? false |
|||
|
|||
let success = service.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse) |
|||
result(success) |
|||
} |
|||
|
|||
/// 释放资源 |
|||
private func handleDispose(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(true) // 已经是nil,直接返回成功 |
|||
return |
|||
} |
|||
|
|||
service.dispose() |
|||
agentService = nil |
|||
|
|||
result(true) |
|||
} |
|||
} |
|||
|
|||
// MARK: - FlutterStreamHandler |
|||
extension SwiftAgentServicePlugin: FlutterStreamHandler { |
|||
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
|||
self.eventSink = events |
|||
return nil |
|||
} |
|||
|
|||
public func onCancel(withArguments arguments: Any?) -> FlutterError? { |
|||
self.eventSink = nil |
|||
return nil |
|||
} |
|||
} |
|||
|
|||
// MARK: - AgentServiceListener |
|||
extension SwiftAgentServicePlugin: AgentServiceListener { |
|||
func onEvent(eventName: String, data: [String: Any]) { |
|||
guard let sink = eventSink else { return } |
|||
|
|||
do { |
|||
// 构建事件JSON |
|||
let eventMap: [String: Any] = [ |
|||
"event": eventName, |
|||
"data": data |
|||
] |
|||
|
|||
// 将事件转换为JSON字符串 |
|||
let jsonData = try JSONSerialization.data(withJSONObject: eventMap, options: []) |
|||
if let jsonString = String(data: jsonData, encoding: .utf8) { |
|||
// 发送到Flutter |
|||
DispatchQueue.main.async { |
|||
sink(jsonString) |
|||
} |
|||
} |
|||
} catch { |
|||
print("事件序列化失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,15 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|||
<plist version="1.0"> |
|||
<dict> |
|||
<key>NSBluetoothAlwaysUsageDescription</key> |
|||
<string>需要蓝牙权限与设备进行通信和传输音频数据</string> |
|||
<key>NSBluetoothPeripheralUsageDescription</key> |
|||
<string>需要蓝牙权限与设备进行通信和传输音频数据</string> |
|||
<key>UIBackgroundModes</key> |
|||
<array> |
|||
<string>bluetooth-central</string> |
|||
<string>audio</string> |
|||
</array> |
|||
</dict> |
|||
</plist> |
|||
@ -0,0 +1,90 @@ |
|||
import Foundation |
|||
import CoreBluetooth |
|||
|
|||
/// BLE服务常量 |
|||
struct BleConst { |
|||
// ================== 协议相关UUID ================== |
|||
/// 主服务UUID - 0000ABC0-0000-1111-2222-123456789ABC |
|||
static let PRIMARY_SERVICE_UUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") |
|||
|
|||
/// 音频服务UUID - 00001801-0000-1000-8000-00805f9b34fb |
|||
static let AUDIO_SERVICE_UUID = CBUUID(string: "0000ae00-0000-1000-8000-00805f9b34fb") |
|||
|
|||
/// 接收音频特征UUID - 0000ABC1-0000-1111-2222-123456789ABC |
|||
static let RECEIVE_AUDIO_CHAR_UUID = CBUUID(string: "0000ae02-0000-1000-8000-00805f9b34fb") |
|||
|
|||
/// 写入特征UUID - 0000ABC1-0000-1111-2222-123456789ABC |
|||
static let WRITE_CHAR_UUID = CBUUID(string: "0000abc1-0000-1111-2222-123456789abc") |
|||
|
|||
/// 通知特征UUID - 0000ABC2-0000-1111-2222-123456789ABC |
|||
static let NOTIFY_CHAR_UUID = CBUUID(string: "0000abc2-0000-1111-2222-123456789abc") |
|||
|
|||
/// 客户端特征配置描述符UUID |
|||
static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "0000fd7b-0000-1000-8000-00805f9b34fb") |
|||
|
|||
/// BLE扫描Action |
|||
static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN" |
|||
|
|||
// ================== 连接状态 ================== |
|||
static let STATE_DISCONNECTED = 0 |
|||
static let STATE_CONNECTING = 1 |
|||
static let STATE_CONNECTED = 2 |
|||
static let STATE_DISCONNECTING = 3 |
|||
static let STATE_ERROR = 4 |
|||
|
|||
// ================== 广播厂商信息 ================== |
|||
/// 厂商ID - 0xABEF |
|||
static let MANUFACTURER_ID: UInt16 = 0xABEF |
|||
/// 客户ID - 0xA001 |
|||
static let CUSTOMER_ID: UInt16 = 0xA001 |
|||
/// 产品ID - 0xA001 |
|||
static let PRODUCT_ID: UInt16 = 0xA001 |
|||
/// 蓝牙状态-未连接 |
|||
static let BT_STATE_DISCONNECTED: UInt8 = 0x00 |
|||
/// 蓝牙状态-已连接 |
|||
static let BT_STATE_CONNECTED: UInt8 = 0x0C |
|||
|
|||
// ================== 协议格式 ================== |
|||
/// APP请求帧头 |
|||
static let APP_REQUEST_HEADER: UInt8 = 0xAA |
|||
/// 设备响应帧头 |
|||
static let DEVICE_RESPONSE_HEADER: UInt8 = 0xBB |
|||
/// 设备主动上报帧头 |
|||
static let DEVICE_NOTIFY_HEADER: UInt8 = 0xCC |
|||
|
|||
// ================== 协议指令 (CMD) ================== |
|||
/// 获取耳机版本号 |
|||
static let CMD_GET_VERSION: UInt8 = 0x01 |
|||
/// 获取耳机产品ID |
|||
static let CMD_GET_PRODUCT_ID: UInt8 = 0x02 |
|||
/// 获取耳机颜色ID |
|||
static let CMD_GET_COLOR_ID: UInt8 = 0x03 |
|||
/// 获取耳机电量ID |
|||
static let CMD_GET_BATTERY_INFO: UInt8 = 0x04 |
|||
/// APP控制编解码指令 |
|||
static let CMD_CONTROL_CODEC: UInt8 = 0x05 |
|||
/// 音量增大 |
|||
static let CMD_VOLUME_UP: UInt8 = 0x06 |
|||
/// 音量减小 |
|||
static let CMD_VOLUME_DOWN: UInt8 = 0x07 |
|||
/// A2DP播放 |
|||
static let CMD_PLAY_A2DP: UInt8 = 0x08 |
|||
/// 唤醒指令 |
|||
static let CMD_WAKE_UP: UInt8 = 0x11 |
|||
|
|||
// ================== 编解码控制值 ================== |
|||
/// 关闭编解码指令 |
|||
static let CODEC_CONTROL_CLOSE: UInt8 = 0x00 |
|||
/// 打开解码指令 |
|||
static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1 |
|||
/// 通过A2DP播放 |
|||
static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2 |
|||
/// 打开编码指令 |
|||
static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1 |
|||
/// 左声道 |
|||
static let AUDIO_CHANNEL_LEFT: UInt8 = 0x01 |
|||
/// 右声道 |
|||
static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02 |
|||
/// 立体声 |
|||
static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03 |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
import Foundation |
|||
|
|||
/// BLE服务回调代理协议 |
|||
protocol BleDelegate: AnyObject { |
|||
/// 扫描结果回调 |
|||
/// - Parameter devices: 设备列表,每个设备包含id、name、rssi和uuids等信息 |
|||
func onScanResult(devices: [[String: Any]]) |
|||
|
|||
/// 连接状态改变回调 |
|||
/// - Parameter state: 连接状态,参考BleConst中的STATE_*常量 |
|||
func onConnectionStateChanged(state: Int) |
|||
|
|||
/// 音频数据接收回调 |
|||
/// - Parameter data: 接收到的音频数据 |
|||
func onAudioDataReceived(data: Data) |
|||
|
|||
/// 唤醒信号接收回调 |
|||
func onWakeupSignalReceived() |
|||
|
|||
/// 设备信息接收回调 |
|||
/// - Parameters: |
|||
/// - infoType: 信息类型 |
|||
/// - infoData: 信息数据 |
|||
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) |
|||
} |
|||
@ -0,0 +1,963 @@ |
|||
import Foundation |
|||
import CoreBluetooth |
|||
import os.log |
|||
|
|||
/// BLE服务类 - 单例实现 |
|||
actor BleService { |
|||
// MARK: - 单例实现 |
|||
static let shared = BleService() |
|||
|
|||
// MARK: - 常量 |
|||
|
|||
/// 用于状态恢复的唯一标识符 |
|||
private let restoreIdentifier = "com.yunqiinnovation.ble_service.restore" |
|||
|
|||
private init() { |
|||
// 初始化连接状态 |
|||
connectionState = BleConst.STATE_DISCONNECTED |
|||
|
|||
// 将通知处理移到非actor方法中 |
|||
setupNotifications() |
|||
} |
|||
|
|||
deinit { |
|||
// 移除通知观察者 |
|||
for observer in notificationObservers { |
|||
NotificationCenter.default.removeObserver(observer) |
|||
} |
|||
} |
|||
|
|||
// MARK: - 属性 |
|||
|
|||
/// 中央管理器 |
|||
private var centralManager: CBCentralManager? |
|||
|
|||
/// 当前连接的外设 |
|||
private var peripheral: CBPeripheral? |
|||
|
|||
/// 主服务特征 |
|||
private var writeCharacteristic: CBCharacteristic? |
|||
private var notifyCharacteristic: CBCharacteristic? |
|||
private var audioCharacteristic: CBCharacteristic? |
|||
|
|||
/// 连接状态 |
|||
private(set) var connectionState: Int { |
|||
didSet { |
|||
if oldValue != connectionState { |
|||
Task { await notifyConnectionStateChanged(state: connectionState) } |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 是否正在扫描 |
|||
private(set) var isScanning = false |
|||
|
|||
/// 扫描结果缓存 |
|||
private var scanResults = [CBPeripheral: [String: Any]]() |
|||
|
|||
/// 扫描超时任务 |
|||
private var scanTimeoutTask: Task<Void, Never>? |
|||
|
|||
/// 是否已初始化 |
|||
private var isInitialized = false |
|||
|
|||
/// 回调代理列表 - 使用actor保证线程安全 |
|||
private var delegates = [BleDelegate]() |
|||
|
|||
/// 日志 |
|||
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService") |
|||
|
|||
/// 通知观察者 |
|||
private var notificationObservers = [NSObjectProtocol]() |
|||
|
|||
// MARK: - 通知设置 |
|||
|
|||
/// 设置应用生命周期通知 |
|||
private nonisolated func setupNotifications() { |
|||
// 使用nonisolated函数以避免actor隔离问题 |
|||
let becomeActiveObserver = NotificationCenter.default.addObserver( |
|||
forName: UIApplication.didBecomeActiveNotification, |
|||
object: nil, |
|||
queue: .main |
|||
) { [weak self] _ in |
|||
guard let self = self else { return } |
|||
Task { |
|||
await self.handleAppDidBecomeActive() |
|||
} |
|||
} |
|||
|
|||
let enterBackgroundObserver = NotificationCenter.default.addObserver( |
|||
forName: UIApplication.didEnterBackgroundNotification, |
|||
object: nil, |
|||
queue: .main |
|||
) { [weak self] _ in |
|||
guard let self = self else { return } |
|||
Task { |
|||
await self.handleAppDidEnterBackground() |
|||
} |
|||
} |
|||
|
|||
// 存储观察者以便后续移除 |
|||
notificationObservers = [becomeActiveObserver, enterBackgroundObserver] |
|||
} |
|||
|
|||
// MARK: - 公共方法 |
|||
|
|||
/// 初始化BLE服务 |
|||
/// - Returns: 是否初始化成功 |
|||
func initialize() -> Bool { |
|||
if isInitialized { |
|||
return true |
|||
} |
|||
|
|||
do { |
|||
// 初始化中央管理器 - 配置状态恢复选项 |
|||
let options: [String: Any] = [ |
|||
CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifier, |
|||
CBCentralManagerOptionShowPowerAlertKey: true |
|||
] |
|||
|
|||
centralManager = CBCentralManager( |
|||
delegate: CBManagerHandler.shared, |
|||
queue: nil, |
|||
options: options |
|||
) |
|||
|
|||
CBManagerHandler.shared.bleService = self |
|||
|
|||
isInitialized = true |
|||
os_log("BLE服务初始化成功", log: logger, type: .info) |
|||
return true |
|||
} catch { |
|||
os_log("BLE服务初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/// 添加回调代理 |
|||
/// - Parameter delegate: 要添加的代理 |
|||
/// - Returns: 是否添加成功 |
|||
func addDelegate(_ delegate: BleDelegate) -> Bool { |
|||
if !delegates.contains(where: { $0 === delegate }) { |
|||
delegates.append(delegate) |
|||
os_log("已添加BLE回调, 当前回调数: %d", log: logger, type: .debug, delegates.count) |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
|
|||
/// 移除回调代理 |
|||
/// - Parameter delegate: 要移除的代理 |
|||
/// - Returns: 是否移除成功 |
|||
func removeDelegate(_ delegate: BleDelegate) -> Bool { |
|||
if let index = delegates.firstIndex(where: { $0 === delegate }) { |
|||
delegates.remove(at: index) |
|||
os_log("已移除BLE回调, 当前回调数: %d", log: logger, type: .debug, delegates.count) |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
|
|||
/// 清除所有回调代理 |
|||
func clearDelegates() { |
|||
delegates.removeAll() |
|||
os_log("已清除所有BLE回调", log: logger, type: .debug) |
|||
} |
|||
|
|||
/// 检查是否已连接 |
|||
/// - Returns: 是否已连接 |
|||
func isConnected() -> Bool { |
|||
return connectionState == BleConst.STATE_CONNECTED |
|||
} |
|||
|
|||
// MARK: - 扫描方法 |
|||
|
|||
/// 开始主动扫描BLE设备 |
|||
/// - Parameter timeout: 扫描超时时间(秒) |
|||
/// - Returns: 是否成功开始扫描 |
|||
func startScan(timeout: TimeInterval = 15.0) -> Bool { |
|||
if !isInitialized { |
|||
os_log("BLE服务未初始化", log: logger, type: .error) |
|||
return false |
|||
} |
|||
|
|||
if isScanning { |
|||
return false |
|||
} |
|||
|
|||
guard let manager = centralManager, manager.state == .poweredOn else { |
|||
os_log("蓝牙未开启", log: logger, type: .error) |
|||
return false |
|||
} |
|||
|
|||
os_log("开始主动扫描BLE设备...", log: logger, type: .info) |
|||
unregisterBackgroundScan() // 避免双重扫描 |
|||
scanResults.removeAll() |
|||
|
|||
// 扫描选项 - 允许重复结果,以获取最新的RSSI值 |
|||
let options: [String: Any] = [ |
|||
CBCentralManagerScanOptionAllowDuplicatesKey: true |
|||
] |
|||
|
|||
// 设置过滤服务UUID |
|||
let scanFilters = [ |
|||
// 如果需要过滤,添加CBUUID |
|||
// CBUUID(string: BleConst.PRIMARY_SERVICE_UUID.uuidString) |
|||
] |
|||
|
|||
// 使用过滤器开始扫描 |
|||
manager.scanForPeripherals( |
|||
withServices: scanFilters.isEmpty ? nil : scanFilters, |
|||
options: options |
|||
) |
|||
|
|||
isScanning = true |
|||
|
|||
// 设置扫描超时 |
|||
scanTimeoutTask?.cancel() |
|||
scanTimeoutTask = Task { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
do { |
|||
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) |
|||
if !Task.isCancelled { |
|||
await self.stopScan() |
|||
} |
|||
} catch { |
|||
// 任务被取消,忽略错误 |
|||
} |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
/// 停止主动扫描 |
|||
/// - Returns: 是否成功停止扫描 |
|||
func stopScan() -> Bool { |
|||
if !isScanning { |
|||
return false |
|||
} |
|||
|
|||
os_log("停止主动扫描", log: logger, type: .info) |
|||
|
|||
centralManager?.stopScan() |
|||
scanTimeoutTask?.cancel() |
|||
scanTimeoutTask = nil |
|||
isScanning = false |
|||
|
|||
// 重新注册后台扫描,保证唤醒能力 |
|||
registerBackgroundScan() |
|||
|
|||
return true |
|||
} |
|||
|
|||
/// 注册后台扫描 |
|||
/// - Returns: 是否成功注册 |
|||
func registerBackgroundScan() -> Bool { |
|||
// iOS中无法直接实现与Android相同的后台扫描唤醒功能 |
|||
os_log("iOS不支持直接的后台扫描唤醒,将使用状态保存和恢复功能", log: logger, type: .info) |
|||
return true |
|||
} |
|||
|
|||
/// 注销后台扫描 |
|||
/// - Returns: 是否成功注销 |
|||
func unregisterBackgroundScan() -> Bool { |
|||
// iOS没有直接的后台扫描注销机制 |
|||
os_log("iOS不支持直接的后台扫描注销", log: logger, type: .info) |
|||
return true |
|||
} |
|||
|
|||
// MARK: - 连接方法 |
|||
|
|||
/// 连接到指定地址的BLE设备 |
|||
/// - Parameter address: 设备UUID字符串 |
|||
func connect(address: String) async { |
|||
if connectionState == BleConst.STATE_CONNECTED { |
|||
os_log("已经连接中,忽略连接请求", log: logger, type: .warning) |
|||
return |
|||
} |
|||
|
|||
guard let central = centralManager, central.state == .poweredOn else { |
|||
updateConnectionState(BleConst.STATE_ERROR) |
|||
return |
|||
} |
|||
|
|||
// 关闭现有连接 |
|||
disconnectGatt() |
|||
|
|||
// 查找对应的外设 |
|||
if let uuid = UUID(uuidString: address), |
|||
let targetPeripheral = scanResults.keys.first(where: { $0.identifier == uuid }) { |
|||
// 如果在扫描结果中找到,直接连接 |
|||
os_log("从扫描结果中找到目标设备,开始连接...", log: logger, type: .debug) |
|||
await connectToPeripheral(targetPeripheral) |
|||
} else { |
|||
// 如果没有找到,尝试从已知设备中恢复 |
|||
os_log("尝试使用UUID恢复设备连接: %{public}@", log: logger, type: .debug, address) |
|||
if let uuid = UUID(uuidString: address) { |
|||
let peripherals = central.retrievePeripherals(withIdentifiers: [uuid]) |
|||
if let targetPeripheral = peripherals.first { |
|||
await connectToPeripheral(targetPeripheral) |
|||
} else { |
|||
os_log("未找到指定UUID的设备: %{public}@", log: logger, type: .error, address) |
|||
updateConnectionState(BleConst.STATE_ERROR) |
|||
} |
|||
} else { |
|||
os_log("无效的UUID格式: %{public}@", log: logger, type: .error, address) |
|||
updateConnectionState(BleConst.STATE_ERROR) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 连接到指定外设 |
|||
/// - Parameter peripheral: 要连接的外设 |
|||
private func connectToPeripheral(_ peripheral: CBPeripheral) async { |
|||
self.peripheral = peripheral |
|||
peripheral.delegate = CBPeripheralHandler.shared |
|||
CBPeripheralHandler.shared.bleService = self |
|||
updateConnectionState(BleConst.STATE_CONNECTING) |
|||
centralManager?.connect(peripheral, options: nil) |
|||
} |
|||
|
|||
/// 断开当前连接 |
|||
func disconnect() async { |
|||
guard let peripheral = peripheral, let central = centralManager else { |
|||
return |
|||
} |
|||
|
|||
updateConnectionState(BleConst.STATE_DISCONNECTING) |
|||
central.cancelPeripheralConnection(peripheral) |
|||
} |
|||
|
|||
/// 安全关闭GATT连接 |
|||
private func disconnectGatt() { |
|||
if let peripheral = peripheral, let central = centralManager { |
|||
central.cancelPeripheralConnection(peripheral) |
|||
} |
|||
|
|||
peripheral = nil |
|||
writeCharacteristic = nil |
|||
notifyCharacteristic = nil |
|||
audioCharacteristic = nil |
|||
} |
|||
|
|||
/// 更新连接状态并通知回调 |
|||
private func updateConnectionState(_ state: Int) { |
|||
connectionState = state |
|||
} |
|||
|
|||
// MARK: - 设备通信方法 |
|||
|
|||
/// 获取设备版本号 |
|||
/// - Returns: 是否成功发送请求 |
|||
func getDeviceVersion() -> Bool { |
|||
return sendCommand(BleConst.CMD_GET_VERSION) |
|||
} |
|||
|
|||
/// 获取电池信息 |
|||
/// - Returns: 是否成功发送请求 |
|||
func getBatteryInfo() -> Bool { |
|||
return sendCommand(BleConst.CMD_GET_BATTERY_INFO) |
|||
} |
|||
|
|||
/// 打开编码器 |
|||
/// - Returns: 是否成功发送请求 |
|||
func openEncoder() -> Bool { |
|||
return sendCodecControl(BleConst.CODEC_CONTROL_ENCODE_ON) |
|||
} |
|||
|
|||
/// 打开解码器 |
|||
/// - Returns: 是否成功发送请求 |
|||
func openDecoder() -> Bool { |
|||
return sendCodecControl(BleConst.CODEC_CONTROL_DECODE_ON) |
|||
} |
|||
|
|||
/// 关闭编解码器 |
|||
/// - Returns: 是否成功发送请求 |
|||
func closeCodec() -> Bool { |
|||
return sendCodecControl(BleConst.CODEC_CONTROL_CLOSE) |
|||
} |
|||
|
|||
/// 播放A2DP |
|||
/// - Returns: 是否成功发送请求 |
|||
func playA2DP() -> Bool { |
|||
return sendCodecControl(BleConst.CODEC_CONTROL_A2DP_PLAY) |
|||
} |
|||
|
|||
/// 发送编解码控制指令 |
|||
/// - Parameter control: 控制值 |
|||
/// - Returns: 是否成功发送 |
|||
private func sendCodecControl(_ control: UInt8) -> Bool { |
|||
let data = Data([BleConst.APP_REQUEST_HEADER, BleConst.CMD_CONTROL_CODEC, control]) |
|||
return writeData(data) |
|||
} |
|||
|
|||
/// 发送命令 |
|||
/// - Parameter command: 命令字 |
|||
/// - Returns: 是否成功发送 |
|||
private func sendCommand(_ command: UInt8) -> Bool { |
|||
let data = Data([BleConst.APP_REQUEST_HEADER, command]) |
|||
return writeData(data) |
|||
} |
|||
|
|||
/// 写入数据 |
|||
/// - Parameter data: 要写入的数据 |
|||
/// - Returns: 是否成功写入 |
|||
private func writeData(_ data: Data) -> Bool { |
|||
guard let peripheral = peripheral, |
|||
let characteristic = writeCharacteristic, |
|||
connectionState == BleConst.STATE_CONNECTED else { |
|||
os_log("无法写入数据,设备未连接或特征未找到", log: logger, type: .error) |
|||
return false |
|||
} |
|||
|
|||
let hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: " ") |
|||
os_log("写入数据: %{public}@", log: logger, type: .debug, hexString) |
|||
|
|||
peripheral.writeValue(data, for: characteristic, type: .withoutResponse) |
|||
return true |
|||
} |
|||
|
|||
// MARK: - 数据处理方法 |
|||
|
|||
/// 处理音频数据 |
|||
/// - Parameter data: 音频数据 |
|||
func processAudioData(_ data: Data) async { |
|||
os_log("收到音频数据: %d字节", log: logger, type: .debug, data.count) |
|||
await notifyAudioDataReceived(data: data) |
|||
} |
|||
|
|||
/// 处理设备响应 |
|||
/// - Parameter data: 设备响应数据 |
|||
func processDeviceResponse(_ data: Data) async { |
|||
guard data.count >= 2 else { return } |
|||
|
|||
let cmd = data[1] |
|||
os_log("收到设备响应: CMD=0x%02X", log: logger, type: .debug, cmd) |
|||
|
|||
switch cmd { |
|||
case BleConst.CMD_GET_VERSION: |
|||
if data.count >= 3 { |
|||
// 解析版本号 |
|||
let version = String(format: "%d.%d.%d", data[2], data.count > 3 ? data[3] : 0, data.count > 4 ? data[4] : 0) |
|||
let infoData: [String: Any] = ["version": version] |
|||
await notifyDeviceInfoReceived(infoType: Int(cmd), infoData: infoData) |
|||
} |
|||
case BleConst.CMD_GET_BATTERY_INFO: |
|||
if data.count >= 3 { |
|||
// 解析电池电量 |
|||
let batteryLevel = Int(data[2]) |
|||
let infoData: [String: Any] = ["level": batteryLevel] |
|||
await notifyDeviceInfoReceived(infoType: Int(cmd), infoData: infoData) |
|||
} |
|||
default: |
|||
os_log("未处理的响应命令: 0x%02X", log: logger, type: .debug, cmd) |
|||
} |
|||
} |
|||
|
|||
/// 处理设备通知 |
|||
/// - Parameter data: 设备通知数据 |
|||
func processDeviceNotification(_ data: Data) async { |
|||
guard data.count >= 2 else { return } |
|||
|
|||
let notifyType = data[1] |
|||
os_log("收到设备通知: Type=0x%02X", log: logger, type: .debug, notifyType) |
|||
|
|||
switch notifyType { |
|||
case BleConst.CMD_WAKE_UP: |
|||
// 设备唤醒信号 |
|||
await notifyWakeupSignalReceived() |
|||
default: |
|||
os_log("未处理的通知类型: 0x%02X", log: logger, type: .debug, notifyType) |
|||
} |
|||
} |
|||
|
|||
// MARK: - 应用状态处理 |
|||
|
|||
/// 处理应用进入活跃状态 |
|||
private func handleAppDidBecomeActive() async { |
|||
os_log("应用进入活跃状态", log: logger, type: .debug) |
|||
// 重新初始化蓝牙管理器,如果需要 |
|||
if centralManager == nil { |
|||
_ = initialize() |
|||
} |
|||
} |
|||
|
|||
/// 处理应用进入后台 |
|||
private func handleAppDidEnterBackground() async { |
|||
os_log("应用进入后台", log: logger, type: .debug) |
|||
// 在iOS中,后台蓝牙操作有一定限制,这里可以做一些清理工作 |
|||
} |
|||
|
|||
// MARK: - 回调通知方法 |
|||
|
|||
/// 通知扫描结果 |
|||
/// - Parameter devices: 设备列表 |
|||
func notifyScanResult(devices: [[String: Any]]) async { |
|||
let currentDelegates = delegates |
|||
|
|||
for delegate in currentDelegates { |
|||
await MainActor.run { |
|||
delegate.onScanResult(devices: devices) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 通知连接状态变化 |
|||
/// - Parameter state: 连接状态 |
|||
func notifyConnectionStateChanged(state: Int) async { |
|||
let currentDelegates = delegates |
|||
|
|||
for delegate in currentDelegates { |
|||
await MainActor.run { |
|||
delegate.onConnectionStateChanged(state: state) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 通知音频数据接收 |
|||
/// - Parameter data: 音频数据 |
|||
func notifyAudioDataReceived(data: Data) async { |
|||
let currentDelegates = delegates |
|||
|
|||
for delegate in currentDelegates { |
|||
await MainActor.run { |
|||
delegate.onAudioDataReceived(data: data) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 通知唤醒信号接收 |
|||
func notifyWakeupSignalReceived() async { |
|||
let currentDelegates = delegates |
|||
|
|||
for delegate in currentDelegates { |
|||
await MainActor.run { |
|||
delegate.onWakeupSignalReceived() |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 通知设备信息接收 |
|||
/// - Parameters: |
|||
/// - infoType: 信息类型 |
|||
/// - infoData: 信息数据 |
|||
func notifyDeviceInfoReceived(infoType: Int, infoData: [String: Any]) async { |
|||
let currentDelegates = delegates |
|||
|
|||
for delegate in currentDelegates { |
|||
await MainActor.run { |
|||
delegate.onDeviceInfoReceived(infoType: infoType, infoData: infoData) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 处理扫描到的设备 |
|||
func handleDiscoveredPeripheral(_ peripheral: CBPeripheral, advertisementData: [String: Any], rssi: NSNumber) async { |
|||
// 打印外设信息 |
|||
os_log("扫描到设备: 名称=%{public}@, ID=%{public}@, RSSI=%d", log: logger, type: .debug, |
|||
peripheral.name ?? "未知", peripheral.identifier.uuidString, rssi.intValue) |
|||
|
|||
// 提取服务UUID |
|||
var uuids: [String] = [] |
|||
if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { |
|||
uuids = serviceUUIDs.map { $0.uuidString } |
|||
} |
|||
|
|||
// 创建设备信息字典 |
|||
let deviceInfo: [String: Any] = [ |
|||
"id": peripheral.identifier.uuidString, |
|||
"name": peripheral.name ?? "未知", |
|||
"rssi": rssi.intValue, |
|||
"uuids": uuids |
|||
] |
|||
|
|||
// 更新扫描缓存 |
|||
scanResults[peripheral] = deviceInfo |
|||
|
|||
// 通知扫描结果 |
|||
await notifyScanResult(devices: [deviceInfo]) |
|||
} |
|||
|
|||
/// 处理连接结果 |
|||
func handleConnectionResult(peripheral: CBPeripheral, success: Bool, error: Error?) async { |
|||
if success { |
|||
os_log("已连接到设备: %{public}@", log: logger, type: .info, peripheral.identifier.uuidString) |
|||
updateConnectionState(BleConst.STATE_CONNECTED) |
|||
|
|||
// 开始服务发现 |
|||
peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) |
|||
} else { |
|||
os_log("连接设备失败: %{public}@", log: logger, type: .error, error?.localizedDescription ?? "未知错误") |
|||
updateConnectionState(BleConst.STATE_ERROR) |
|||
|
|||
// 清理资源 |
|||
self.peripheral = nil |
|||
} |
|||
} |
|||
|
|||
/// 处理断开连接 |
|||
func handleDisconnection(peripheral: CBPeripheral, error: Error?) async { |
|||
if let error = error { |
|||
os_log("设备连接断开(错误): %{public}@", log: logger, type: .error, error.localizedDescription) |
|||
updateConnectionState(BleConst.STATE_ERROR) |
|||
} else { |
|||
os_log("设备连接断开(正常)", log: logger, type: .info) |
|||
updateConnectionState(BleConst.STATE_DISCONNECTED) |
|||
} |
|||
|
|||
// 清理资源 |
|||
if self.peripheral?.identifier == peripheral.identifier { |
|||
self.peripheral = nil |
|||
self.writeCharacteristic = nil |
|||
self.notifyCharacteristic = nil |
|||
self.audioCharacteristic = nil |
|||
} |
|||
} |
|||
|
|||
/// 处理发现服务 |
|||
func handleDiscoveredServices(peripheral: CBPeripheral, error: Error?) async { |
|||
if let error = error { |
|||
os_log("发现服务失败: %{public}@", log: logger, type: .error, error.localizedDescription) |
|||
updateConnectionState(BleConst.STATE_ERROR) |
|||
return |
|||
} |
|||
|
|||
guard let services = peripheral.services else { |
|||
os_log("没有发现任何服务", log: logger, type: .error) |
|||
updateConnectionState(BleConst.STATE_ERROR) |
|||
return |
|||
} |
|||
|
|||
// 发现所有服务的特征 |
|||
for service in services { |
|||
os_log("发现服务: %{public}@", log: logger, type: .debug, service.uuid.uuidString) |
|||
peripheral.discoverCharacteristics(nil, for: service) |
|||
} |
|||
} |
|||
|
|||
/// 处理发现的特征 |
|||
func handleDiscoveredCharacteristics(for service: CBService, peripheral: CBPeripheral, error: Error?) async { |
|||
if let error = error { |
|||
os_log("发现特征失败: %{public}@", log: logger, type: .error, error.localizedDescription) |
|||
return |
|||
} |
|||
|
|||
guard let characteristics = service.characteristics else { |
|||
os_log("服务没有任何特征: %{public}@", log: logger, type: .warning, service.uuid.uuidString) |
|||
return |
|||
} |
|||
|
|||
// 根据UUID识别和保存特征 |
|||
for characteristic in characteristics { |
|||
os_log("发现特征: %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString) |
|||
|
|||
// 主服务特征 |
|||
if service.uuid == BleConst.PRIMARY_SERVICE_UUID { |
|||
if characteristic.uuid == BleConst.WRITE_CHAR_UUID { |
|||
writeCharacteristic = characteristic |
|||
os_log("找到写入特征", log: logger, type: .debug) |
|||
} else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { |
|||
notifyCharacteristic = characteristic |
|||
os_log("找到通知特征", log: logger, type: .debug) |
|||
// 设置通知 |
|||
await setupNotification(for: characteristic, peripheral: peripheral) |
|||
} |
|||
} |
|||
|
|||
// 音频服务特征 |
|||
if service.uuid == BleConst.AUDIO_SERVICE_UUID { |
|||
if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { |
|||
audioCharacteristic = characteristic |
|||
os_log("找到音频特征", log: logger, type: .debug) |
|||
// 设置通知 |
|||
await setupNotification(for: characteristic, peripheral: peripheral) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 检查是否找到所有必要的特征 |
|||
if writeCharacteristic == nil || notifyCharacteristic == nil { |
|||
os_log("未找到所有必要的特征", log: logger, type: .error) |
|||
updateConnectionState(BleConst.STATE_ERROR) |
|||
} |
|||
} |
|||
|
|||
/// 设置特征的通知 |
|||
/// - Parameters: |
|||
/// - characteristic: 特征 |
|||
/// - peripheral: 外设 |
|||
private func setupNotification(for characteristic: CBCharacteristic, peripheral: CBPeripheral) async { |
|||
if characteristic.properties.contains(.notify) { |
|||
peripheral.setNotifyValue(true, for: characteristic) |
|||
} |
|||
} |
|||
|
|||
/// 处理特征值更新 |
|||
func handleValueUpdate(for characteristic: CBCharacteristic, peripheral: CBPeripheral, error: Error?) async { |
|||
if let error = error { |
|||
os_log("特征值更新失败: %{public}@", log: logger, type: .error, error.localizedDescription) |
|||
return |
|||
} |
|||
|
|||
guard let data = characteristic.value else { |
|||
os_log("特征没有值", log: logger, type: .warning) |
|||
return |
|||
} |
|||
|
|||
let hexString = data.map { String(format: "%02X", $0) }.joined(separator: " ") |
|||
os_log("收到特征数据: %{public}@ - %{public}@", log: logger, type: .debug, characteristic.uuid.uuidString, hexString) |
|||
|
|||
// 根据特征UUID处理不同类型的数据 |
|||
if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { |
|||
// 音频数据处理 |
|||
await processAudioData(data) |
|||
} else if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID { |
|||
// 通知特征处理 |
|||
if !data.isEmpty { |
|||
switch data[0] { |
|||
case BleConst.DEVICE_RESPONSE_HEADER: |
|||
// 设备响应 |
|||
await processDeviceResponse(data) |
|||
case BleConst.DEVICE_NOTIFY_HEADER: |
|||
// 设备通知 |
|||
await processDeviceNotification(data) |
|||
default: |
|||
os_log("未知数据类型: %d", log: logger, type: .debug, data[0]) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - 状态恢复处理 |
|||
|
|||
/// 处理蓝牙状态恢复 |
|||
func handleStateRestoration(dict: [String: Any]) async { |
|||
os_log("正在恢复蓝牙状态...", log: logger, type: .info) |
|||
|
|||
// 1. 恢复之前连接的外设 |
|||
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { |
|||
os_log("恢复 %d 个已连接的外设", log: logger, type: .debug, peripherals.count) |
|||
|
|||
for peripheral in peripherals { |
|||
self.peripheral = peripheral |
|||
peripheral.delegate = CBPeripheralHandler.shared |
|||
CBPeripheralHandler.shared.bleService = self |
|||
|
|||
// 根据外设当前状态采取不同操作 |
|||
if peripheral.state == .connected { |
|||
os_log("外设 %{public}@ 已连接", log: logger, type: .debug, peripheral.identifier.uuidString) |
|||
updateConnectionState(BleConst.STATE_CONNECTED) |
|||
|
|||
// 重新发现服务 |
|||
peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]) |
|||
} else if peripheral.state == .connecting { |
|||
os_log("外设 %{public}@ 正在连接中", log: logger, type: .debug, peripheral.identifier.uuidString) |
|||
updateConnectionState(BleConst.STATE_CONNECTING) |
|||
} else { |
|||
os_log("外设 %{public}@ 状态异常: %d", log: logger, type: .debug, peripheral.identifier.uuidString, peripheral.state.rawValue) |
|||
// 尝试重新连接 |
|||
updateConnectionState(BleConst.STATE_CONNECTING) |
|||
centralManager?.connect(peripheral, options: nil) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 2. 恢复扫描状态 |
|||
if let scanServices = dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] { |
|||
os_log("恢复扫描任务,服务UUID数: %d", log: logger, type: .debug, scanServices.count) |
|||
|
|||
let scanOptions = dict[CBCentralManagerRestoredStateScanOptionsKey] as? [String: Any] ?? [ |
|||
CBCentralManagerScanOptionAllowDuplicatesKey: true |
|||
] |
|||
|
|||
// 重新启动扫描任务 |
|||
centralManager?.scanForPeripherals( |
|||
withServices: scanServices.isEmpty ? nil : scanServices, |
|||
options: scanOptions |
|||
) |
|||
|
|||
isScanning = true |
|||
|
|||
// 设置新的扫描超时(默认15秒) |
|||
scanTimeoutTask?.cancel() |
|||
scanTimeoutTask = Task { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
do { |
|||
try await Task.sleep(nanoseconds: UInt64(15.0 * 1_000_000_000)) |
|||
if !Task.isCancelled { |
|||
await self.stopScan() |
|||
} |
|||
} catch { |
|||
// 任务被取消,忽略错误 |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 通知外部:BLE状态已恢复 |
|||
await notifyStateRestored() |
|||
} |
|||
|
|||
/// 通知状态恢复 |
|||
private func notifyStateRestored() async { |
|||
let currentDelegates = delegates |
|||
|
|||
for delegate in currentDelegates { |
|||
if let stateDelegate = delegate as? BleStateRestorationDelegate { |
|||
await MainActor.run { |
|||
stateDelegate.onBleStateRestored() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - 中央管理器委托处理器 |
|||
|
|||
class CBManagerHandler: NSObject, CBCentralManagerDelegate { |
|||
static let shared = CBManagerHandler() |
|||
weak var bleService: BleService? |
|||
|
|||
func centralManagerDidUpdateState(_ central: CBCentralManager) { |
|||
Task { |
|||
guard let service = bleService else { return } |
|||
|
|||
switch central.state { |
|||
case .poweredOn: |
|||
await MainActor.run { |
|||
os_log("蓝牙已开启", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .info) |
|||
} |
|||
case .poweredOff: |
|||
await MainActor.run { |
|||
os_log("蓝牙已关闭", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .warning) |
|||
} |
|||
await service.updateConnectionState(BleConst.STATE_DISCONNECTED) |
|||
case .unauthorized, .unsupported: |
|||
await MainActor.run { |
|||
os_log("蓝牙不可用: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, central.state.rawValue) |
|||
} |
|||
await service.updateConnectionState(BleConst.STATE_ERROR) |
|||
default: |
|||
await MainActor.run { |
|||
os_log("蓝牙状态变化: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, central.state.rawValue) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { |
|||
Task { |
|||
await bleService?.handleDiscoveredPeripheral(peripheral, advertisementData: advertisementData, rssi: RSSI) |
|||
} |
|||
} |
|||
|
|||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { |
|||
Task { |
|||
await bleService?.handleConnectionResult(peripheral: peripheral, success: true, error: nil) |
|||
} |
|||
} |
|||
|
|||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { |
|||
Task { |
|||
await bleService?.handleConnectionResult(peripheral: peripheral, success: false, error: error) |
|||
} |
|||
} |
|||
|
|||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { |
|||
Task { |
|||
await bleService?.handleDisconnection(peripheral: peripheral, error: error) |
|||
} |
|||
} |
|||
|
|||
/// 状态恢复处理 |
|||
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { |
|||
os_log("蓝牙状态恢复被触发", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .info) |
|||
|
|||
// 打印恢复信息 |
|||
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { |
|||
os_log("恢复外设数量: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, peripherals.count) |
|||
for (i, peripheral) in peripherals.enumerated() { |
|||
os_log("恢复外设[%d]: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, i, peripheral.identifier.uuidString) |
|||
} |
|||
} |
|||
|
|||
if let services = dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] { |
|||
os_log("恢复扫描服务数量: %d", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, services.count) |
|||
} |
|||
|
|||
// 异步处理状态恢复 |
|||
Task { |
|||
if let service = bleService { |
|||
await service.handleStateRestoration(dict: dict) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - 外设委托处理器 |
|||
|
|||
class CBPeripheralHandler: NSObject, CBPeripheralDelegate { |
|||
static let shared = CBPeripheralHandler() |
|||
weak var bleService: BleService? |
|||
|
|||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { |
|||
Task { |
|||
await bleService?.handleDiscoveredServices(peripheral: peripheral, error: error) |
|||
} |
|||
} |
|||
|
|||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { |
|||
Task { |
|||
await bleService?.handleDiscoveredCharacteristics(for: service, peripheral: peripheral, error: error) |
|||
} |
|||
} |
|||
|
|||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { |
|||
Task { |
|||
await bleService?.handleValueUpdate(for: characteristic, peripheral: peripheral, error: error) |
|||
} |
|||
} |
|||
|
|||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { |
|||
if let error = error { |
|||
os_log("写入特征值失败: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, error.localizedDescription) |
|||
} else { |
|||
os_log("写入特征值成功", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug) |
|||
} |
|||
} |
|||
|
|||
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { |
|||
if let error = error { |
|||
os_log("设置通知状态失败: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .error, error.localizedDescription) |
|||
} else { |
|||
os_log("设置通知状态成功: %{public}@", log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), type: .debug, characteristic.uuid.uuidString) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - BLE状态恢复代理 |
|||
/// 蓝牙状态恢复代理 |
|||
protocol BleStateRestorationDelegate: AnyObject { |
|||
/// 当蓝牙状态被系统恢复时调用 |
|||
func onBleStateRestored() |
|||
} |
|||
|
|||
// MARK: - BLE代理 |
|||
/// 蓝牙事件代理 |
|||
protocol BleDelegate: AnyObject { |
|||
/// 扫描结果回调 |
|||
func onScanResult(devices: [[String: Any]]) |
|||
|
|||
/// 连接状态变化回调 |
|||
func onConnectionStateChanged(state: Int) |
|||
|
|||
/// 音频数据接收回调 |
|||
func onAudioDataReceived(data: Data) |
|||
|
|||
/// 唤醒信号接收回调 |
|||
func onWakeupSignalReceived() |
|||
|
|||
/// 设备信息接收回调 |
|||
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
#import <Flutter/Flutter.h> |
|||
|
|||
@interface BleServicePlugin : NSObject<FlutterPlugin> |
|||
@end |
|||
@ -0,0 +1,12 @@ |
|||
#import "BleServicePlugin.h" |
|||
#if __has_include(<ble_service/ble_service-Swift.h>) |
|||
#import <ble_service/ble_service-Swift.h> |
|||
#else |
|||
#import "ble_service-Swift.h" |
|||
#endif |
|||
|
|||
@implementation BleServicePlugin |
|||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
|||
[SwiftBleServicePlugin registerWithRegistrar:registrar]; |
|||
} |
|||
@end |
|||
@ -0,0 +1,203 @@ |
|||
import Flutter |
|||
import UIKit |
|||
|
|||
public class SwiftBleServicePlugin: NSObject, FlutterPlugin, BleDelegate { |
|||
private let TAG = "BleServicePlugin" |
|||
|
|||
// 方法通道 |
|||
private var methodChannel: FlutterMethodChannel? |
|||
|
|||
// 事件通道 |
|||
private var dataEventSink: FlutterEventSink? |
|||
private var statusEventSink: FlutterEventSink? |
|||
|
|||
// BLE服务实例 |
|||
private let bleService = BleService.shared |
|||
|
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger()) |
|||
|
|||
// 创建插件实例 |
|||
let instance = SwiftBleServicePlugin() |
|||
instance.methodChannel = methodChannel |
|||
|
|||
// 设置方法通道处理器 |
|||
registrar.addMethodCallDelegate(instance, channel: methodChannel) |
|||
|
|||
// 设置事件通道 |
|||
instance.setupEventChannels(with: registrar) |
|||
|
|||
// 初始化BLE服务 |
|||
_ = instance.bleService.initialize() |
|||
|
|||
// 添加自身作为BLE回调 |
|||
_ = instance.bleService.addDelegate(instance) |
|||
} |
|||
|
|||
/// 设置事件通道 |
|||
private func setupEventChannels(with registrar: FlutterPluginRegistrar) { |
|||
// 数据接收事件通道 |
|||
let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger()) |
|||
dataEventChannel.setStreamHandler(StreamHandler { [weak self] sink in |
|||
self?.dataEventSink = sink |
|||
} onCancel: { [weak self] in |
|||
self?.dataEventSink = nil |
|||
}) |
|||
|
|||
// 状态更新事件通道 |
|||
let statusEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/status_events", binaryMessenger: registrar.messenger()) |
|||
statusEventChannel.setStreamHandler(StreamHandler { [weak self] sink in |
|||
self?.statusEventSink = sink |
|||
} onCancel: { [weak self] in |
|||
self?.statusEventSink = nil |
|||
}) |
|||
} |
|||
|
|||
/// 发送事件到Flutter |
|||
private func sendEvent(_ eventSink: FlutterEventSink?, _ data: Any) { |
|||
DispatchQueue.main.async { |
|||
eventSink?(data) |
|||
} |
|||
} |
|||
|
|||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
NSLog("Method call: \(call.method)") |
|||
|
|||
switch call.method { |
|||
case "isConnected": |
|||
result(bleService.isConnected()) |
|||
|
|||
case "getBatteryInfo": |
|||
result(bleService.getBatteryInfo()) |
|||
|
|||
case "getDeviceVersion": |
|||
result(bleService.getDeviceVersion()) |
|||
|
|||
case "openEncoder": |
|||
result(bleService.openEncoder()) |
|||
|
|||
case "openDecoder": |
|||
result(bleService.openDecoder()) |
|||
|
|||
case "closeCodec": |
|||
result(bleService.closeCodec()) |
|||
|
|||
case "playA2DP": |
|||
result(bleService.playA2DP()) |
|||
|
|||
case "registerBackgroundScan": |
|||
result(bleService.registerBackgroundScan()) |
|||
|
|||
case "unregisterBackgroundScan": |
|||
result(bleService.unregisterBackgroundScan()) |
|||
|
|||
case "startBleService": |
|||
guard let config = call.arguments as? [String: Any] else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "配置参数无效", details: nil)) |
|||
return |
|||
} |
|||
|
|||
// 处理配置参数 |
|||
if let address = config["mac"] as? String { |
|||
// 如果提供了设备地址,尝试连接 |
|||
bleService.connect(address: address) |
|||
} |
|||
|
|||
// 开始扫描,寻找设备 |
|||
let scanSuccess = bleService.startScan() |
|||
result(scanSuccess) |
|||
|
|||
case "stopBleService": |
|||
// 停止扫描和断开连接 |
|||
_ = bleService.stopScan() |
|||
if bleService.isConnected() { |
|||
bleService.disconnect() |
|||
} |
|||
result(true) |
|||
|
|||
default: |
|||
result(FlutterMethodNotImplemented) |
|||
} |
|||
} |
|||
|
|||
// MARK: - BleDelegate Methods |
|||
|
|||
func onScanResult(devices: [[String: Any]]) { |
|||
// 目前Flutter层没有直接使用此事件,可以选择不转发 |
|||
} |
|||
|
|||
func onConnectionStateChanged(state: Int) { |
|||
// 将连接状态变化通过状态通道发送 |
|||
let stateStr: String |
|||
switch state { |
|||
case BleConst.STATE_DISCONNECTED: |
|||
stateStr = "Disconnected" |
|||
case BleConst.STATE_CONNECTING: |
|||
stateStr = "Connecting" |
|||
case BleConst.STATE_CONNECTED: |
|||
stateStr = "Connected" |
|||
case BleConst.STATE_DISCONNECTING: |
|||
stateStr = "Disconnecting" |
|||
case BleConst.STATE_ERROR: |
|||
stateStr = "Connection Error" |
|||
default: |
|||
stateStr = "Unknown State(\(state))" |
|||
} |
|||
|
|||
let stateMap: [String: Any] = [ |
|||
"type": "connectionStateChanged", |
|||
"state": state, |
|||
"stateStr": stateStr |
|||
] |
|||
|
|||
sendEvent(statusEventSink, stateMap) |
|||
} |
|||
|
|||
func onAudioDataReceived(data: Data) { |
|||
// 将音频数据通过数据通道发送 |
|||
let dataMap: [String: Any] = [ |
|||
"type": "audioData", |
|||
"data": FlutterStandardTypedData(bytes: data) |
|||
] |
|||
|
|||
sendEvent(dataEventSink, dataMap) |
|||
} |
|||
|
|||
func onWakeupSignalReceived() { |
|||
// 将唤醒事件通过状态通道发送 |
|||
sendEvent(statusEventSink, ["type": "wakeup"]) |
|||
} |
|||
|
|||
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { |
|||
// 将设备信息通过状态通道发送 |
|||
var resultMap = infoData |
|||
resultMap["type"] = "deviceInfo" |
|||
resultMap["result"] = infoType |
|||
|
|||
sendEvent(statusEventSink, resultMap) |
|||
} |
|||
} |
|||
|
|||
// MARK: - 事件处理器类 |
|||
|
|||
/// 简化的事件处理器类 |
|||
class StreamHandler: NSObject, FlutterStreamHandler { |
|||
private let onListen: (FlutterEventSink) -> Void |
|||
private let onCancel: () -> Void |
|||
|
|||
init(onListen: @escaping (FlutterEventSink) -> Void, onCancel: @escaping () -> Void) { |
|||
self.onListen = onListen |
|||
self.onCancel = onCancel |
|||
super.init() |
|||
} |
|||
|
|||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
|||
onListen(events) |
|||
return nil |
|||
} |
|||
|
|||
func onCancel(withArguments arguments: Any?) -> FlutterError? { |
|||
onCancel() |
|||
return nil |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
# |
|||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
|||
# Run `pod lib lint ble_service.podspec` to validate before publishing. |
|||
# |
|||
Pod::Spec.new do |s| |
|||
s.name = 'ble_service' |
|||
s.version = '0.0.1' |
|||
s.summary = 'BLE服务Flutter插件' |
|||
s.description = <<-DESC |
|||
BLE服务Flutter插件,提供蓝牙设备扫描、连接和通信功能。 |
|||
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, '11.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' |
|||
end |
|||
@ -0,0 +1,410 @@ |
|||
import Foundation |
|||
import SQLite3 |
|||
|
|||
/// ChatStorageHelper - iOS版本的聊天存储助手 |
|||
/// 负责SQLite数据库操作和业务逻辑 |
|||
class ChatStorageHelper { |
|||
// MARK: - Constants |
|||
private let TAG = "ChatStorageHelper" |
|||
|
|||
// 数据库常量 |
|||
private let DATABASE_NAME = "chat_messages.db" |
|||
private let DATABASE_VERSION = 1 |
|||
|
|||
// 表和列名 |
|||
private let TABLE_MESSAGES = "messages" |
|||
private let COLUMN_ID = "id" |
|||
private let COLUMN_SESSION_ID = "session_id" |
|||
private let COLUMN_MESSAGE = "message" |
|||
private let COLUMN_SENDER = "sender" |
|||
private let COLUMN_TIMESTAMP = "timestamp" |
|||
private let COLUMN_METADATA = "metadata" |
|||
|
|||
// MARK: - Properties |
|||
private var db: OpaquePointer? |
|||
private let dbPath: String |
|||
|
|||
// MARK: - Initialization |
|||
init(_ context: Any) { |
|||
// 获取数据库文件路径 |
|||
let fileManager = FileManager.default |
|||
if let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { |
|||
let databaseDirectory = documentsDirectory.appendingPathComponent("Databases") |
|||
|
|||
// 确保目录存在 |
|||
try? fileManager.createDirectory(at: databaseDirectory, withIntermediateDirectories: true) |
|||
|
|||
dbPath = databaseDirectory.appendingPathComponent(DATABASE_NAME).path |
|||
|
|||
// 打开数据库连接 |
|||
openDatabase() |
|||
} else { |
|||
dbPath = "" |
|||
print("\(TAG) 无法获取文档目录路径") |
|||
} |
|||
} |
|||
|
|||
deinit { |
|||
closeDatabase() |
|||
} |
|||
|
|||
// MARK: - Database Operations |
|||
|
|||
/// 打开数据库连接 |
|||
private func openDatabase() { |
|||
if sqlite3_open(dbPath, &db) == SQLITE_OK { |
|||
createTables() |
|||
print("\(TAG) 数据库已成功打开") |
|||
} else { |
|||
print("\(TAG) 无法打开数据库: \(String(describing: errorMessage))") |
|||
} |
|||
} |
|||
|
|||
/// 关闭数据库连接 |
|||
private func closeDatabase() { |
|||
if db != nil { |
|||
sqlite3_close(db) |
|||
db = nil |
|||
} |
|||
} |
|||
|
|||
/// 创建必要的数据库表 |
|||
private func createTables() { |
|||
// 创建消息表 |
|||
let createTableSQL = """ |
|||
CREATE TABLE IF NOT EXISTS \(TABLE_MESSAGES) ( |
|||
\(COLUMN_ID) INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
\(COLUMN_SESSION_ID) TEXT NOT NULL, |
|||
\(COLUMN_MESSAGE) TEXT NOT NULL, |
|||
\(COLUMN_SENDER) TEXT NOT NULL, |
|||
\(COLUMN_TIMESTAMP) TEXT NOT NULL, |
|||
\(COLUMN_METADATA) TEXT |
|||
); |
|||
""" |
|||
|
|||
if executeUpdate(sql: createTableSQL) { |
|||
// 创建索引以加快查询速度 |
|||
executeUpdate(sql: "CREATE INDEX IF NOT EXISTS idx_session_id ON \(TABLE_MESSAGES) (\(COLUMN_SESSION_ID))") |
|||
executeUpdate(sql: "CREATE INDEX IF NOT EXISTS idx_timestamp ON \(TABLE_MESSAGES) (\(COLUMN_TIMESTAMP))") |
|||
} |
|||
} |
|||
|
|||
/// 获取SQLite错误消息 |
|||
private var errorMessage: String { |
|||
if let errorPointer = sqlite3_errmsg(db) { |
|||
let errorMessage = String(cString: errorPointer) |
|||
return errorMessage |
|||
} else { |
|||
return "未知错误" |
|||
} |
|||
} |
|||
|
|||
/// 执行SQL更新操作(INSERT, UPDATE, DELETE, CREATE等) |
|||
private func executeUpdate(sql: String, parameters: [Any]? = nil) -> Bool { |
|||
var statement: OpaquePointer? |
|||
|
|||
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK { |
|||
// 绑定参数 |
|||
if let params = parameters { |
|||
for (index, param) in params.enumerated() { |
|||
bindParameter(statement: statement, index: index + 1, value: param) |
|||
} |
|||
} |
|||
|
|||
// 执行语句 |
|||
let result = sqlite3_step(statement) |
|||
sqlite3_finalize(statement) |
|||
|
|||
return result == SQLITE_DONE |
|||
} else { |
|||
print("\(TAG) 准备SQL语句失败: \(errorMessage)") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/// 绑定SQL参数 |
|||
private func bindParameter(statement: OpaquePointer?, index: Int, value: Any) { |
|||
if let value = value as? Int { |
|||
sqlite3_bind_int(statement, Int32(index), Int32(value)) |
|||
} else if let value = value as? Double { |
|||
sqlite3_bind_double(statement, Int32(index), value) |
|||
} else if let value = value as? String { |
|||
sqlite3_bind_text(statement, Int32(index), (value as NSString).utf8String, -1, nil) |
|||
} else if value is NSNull { |
|||
sqlite3_bind_null(statement, Int32(index)) |
|||
} |
|||
} |
|||
|
|||
/// 查询数据并处理结果 |
|||
private func executeQuery(sql: String, parameters: [Any]? = nil, handler: (OpaquePointer) -> Void) { |
|||
var statement: OpaquePointer? |
|||
|
|||
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK { |
|||
// 绑定参数 |
|||
if let params = parameters { |
|||
for (index, param) in params.enumerated() { |
|||
bindParameter(statement: statement, index: index + 1, value: param) |
|||
} |
|||
} |
|||
|
|||
// 处理结果 |
|||
while sqlite3_step(statement) == SQLITE_ROW { |
|||
handler(statement!) |
|||
} |
|||
} else { |
|||
print("\(TAG) 执行查询失败: \(errorMessage)") |
|||
} |
|||
|
|||
sqlite3_finalize(statement) |
|||
} |
|||
|
|||
// MARK: - Public Methods |
|||
|
|||
/// 保存消息 |
|||
/// - Parameters: |
|||
/// - sessionId: 会话ID |
|||
/// - message: 消息内容 |
|||
/// - sender: 发送者 |
|||
/// - metadata: 元数据 |
|||
/// - Returns: 成功返回消息ID,失败返回-1 |
|||
func saveMessage(sessionId: String, message: String, sender: String, metadata: String? = nil) -> Int64 { |
|||
let dateFormatter = DateFormatter() |
|||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" |
|||
let timestamp = dateFormatter.string(from: Date()) |
|||
|
|||
let sql = """ |
|||
INSERT INTO \(TABLE_MESSAGES) (\(COLUMN_SESSION_ID), \(COLUMN_MESSAGE), \(COLUMN_SENDER), \(COLUMN_TIMESTAMP), \(COLUMN_METADATA)) |
|||
VALUES (?, ?, ?, ?, ?) |
|||
""" |
|||
|
|||
var parameters: [Any] = [sessionId, message, sender, timestamp] |
|||
if let metadata = metadata { |
|||
parameters.append(metadata) |
|||
} else { |
|||
parameters.append(NSNull()) |
|||
} |
|||
|
|||
if executeUpdate(sql: sql, parameters: parameters) { |
|||
return sqlite3_last_insert_rowid(db) |
|||
} else { |
|||
print("\(TAG) 保存消息失败") |
|||
return -1 |
|||
} |
|||
} |
|||
|
|||
/// 获取指定会话的消息 |
|||
/// - Parameters: |
|||
/// - sessionId: 会话ID |
|||
/// - page: 页码 |
|||
/// - pageSize: 每页大小 |
|||
/// - Returns: JSON字符串,包含消息列表和分页信息 |
|||
func getMessages(sessionId: String, page: Int = 1, pageSize: Int = 20) -> [String: Any] { |
|||
var messages: [[String: Any]] = [] |
|||
let offset = (page - 1) * pageSize |
|||
|
|||
// 查询消息 |
|||
let sql = """ |
|||
SELECT * FROM \(TABLE_MESSAGES) |
|||
WHERE \(COLUMN_SESSION_ID) = ? |
|||
ORDER BY \(COLUMN_TIMESTAMP) DESC |
|||
LIMIT ? OFFSET ? |
|||
""" |
|||
|
|||
executeQuery(sql: sql, parameters: [sessionId, pageSize, offset]) { statement in |
|||
var message: [String: Any] = [:] |
|||
|
|||
// 获取ID |
|||
message["id"] = Int(sqlite3_column_int(statement, 0)) |
|||
|
|||
// 获取会话ID |
|||
if let sessionId = sqlite3_column_text(statement, 1) { |
|||
message["sessionId"] = String(cString: sessionId) |
|||
} |
|||
|
|||
// 获取消息内容 |
|||
if let messageText = sqlite3_column_text(statement, 2) { |
|||
message["message"] = String(cString: messageText) |
|||
} |
|||
|
|||
// 获取发送者 |
|||
if let sender = sqlite3_column_text(statement, 3) { |
|||
message["sender"] = String(cString: sender) |
|||
} |
|||
|
|||
// 获取时间戳 |
|||
if let timestamp = sqlite3_column_text(statement, 4) { |
|||
message["timestamp"] = String(cString: timestamp) |
|||
} |
|||
|
|||
// 获取元数据(如果有) |
|||
if sqlite3_column_type(statement, 5) != SQLITE_NULL { |
|||
if let metadata = sqlite3_column_text(statement, 5) { |
|||
message["metadata"] = String(cString: metadata) |
|||
} |
|||
} |
|||
|
|||
messages.append(message) |
|||
} |
|||
|
|||
// 获取总记录数 |
|||
var totalCount = 0 |
|||
let countSql = "SELECT COUNT(*) FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" |
|||
|
|||
executeQuery(sql: countSql, parameters: [sessionId]) { statement in |
|||
totalCount = Int(sqlite3_column_int(statement, 0)) |
|||
} |
|||
|
|||
// 计算总页数 |
|||
let totalPages = (totalCount + pageSize - 1) / pageSize |
|||
|
|||
return [ |
|||
"messages": messages, |
|||
"page": page, |
|||
"pageSize": pageSize, |
|||
"totalCount": totalCount, |
|||
"totalPages": totalPages |
|||
] |
|||
} |
|||
|
|||
/// 获取会话列表 |
|||
/// - Returns: 会话列表数据 |
|||
func getSessions() -> [[String: Any]] { |
|||
var sessions: [[String: Any]] = [] |
|||
|
|||
// 查询不同的会话ID及其最新消息 |
|||
let sql = """ |
|||
SELECT s1.\(COLUMN_SESSION_ID), s1.\(COLUMN_MESSAGE), s1.\(COLUMN_SENDER), s1.\(COLUMN_TIMESTAMP) |
|||
FROM \(TABLE_MESSAGES) s1 |
|||
INNER JOIN ( |
|||
SELECT \(COLUMN_SESSION_ID), MAX(\(COLUMN_TIMESTAMP)) as max_timestamp |
|||
FROM \(TABLE_MESSAGES) |
|||
GROUP BY \(COLUMN_SESSION_ID) |
|||
) s2 ON s1.\(COLUMN_SESSION_ID) = s2.\(COLUMN_SESSION_ID) AND s1.\(COLUMN_TIMESTAMP) = s2.max_timestamp |
|||
ORDER BY s1.\(COLUMN_TIMESTAMP) DESC |
|||
""" |
|||
|
|||
executeQuery(sql: sql) { statement in |
|||
var session: [String: Any] = [:] |
|||
|
|||
// 获取会话ID |
|||
if let sessionId = sqlite3_column_text(statement, 0) { |
|||
let sessionIdStr = String(cString: sessionId) |
|||
session["sessionId"] = sessionIdStr |
|||
|
|||
// 获取该会话的消息数量 |
|||
var messageCount = 0 |
|||
let countSql = "SELECT COUNT(*) FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" |
|||
|
|||
executeQuery(sql: countSql, parameters: [sessionIdStr]) { countStmt in |
|||
messageCount = Int(sqlite3_column_int(countStmt, 0)) |
|||
} |
|||
|
|||
session["messageCount"] = messageCount |
|||
} |
|||
|
|||
// 获取最后一条消息 |
|||
if let message = sqlite3_column_text(statement, 1) { |
|||
session["lastMessage"] = String(cString: message) |
|||
} |
|||
|
|||
// 获取最后发送者 |
|||
if let sender = sqlite3_column_text(statement, 2) { |
|||
session["lastSender"] = String(cString: sender) |
|||
} |
|||
|
|||
// 获取时间戳 |
|||
if let timestamp = sqlite3_column_text(statement, 3) { |
|||
session["timestamp"] = String(cString: timestamp) |
|||
} |
|||
|
|||
sessions.append(session) |
|||
} |
|||
|
|||
return sessions |
|||
} |
|||
|
|||
/// 删除消息 |
|||
/// - Parameters: |
|||
/// - sessionId: 会话ID,如果提供则删除整个会话 |
|||
/// - messageIds: 消息ID列表,如果提供则删除指定消息 |
|||
/// - Returns: 成功返回true,失败返回false |
|||
func deleteMessages(sessionId: String? = nil, messageIds: [Int]? = nil) -> Bool { |
|||
if let messageIds = messageIds, !messageIds.isEmpty { |
|||
// 删除指定ID的消息 |
|||
let placeholders = Array(repeating: "?", count: messageIds.count).joined(separator: ",") |
|||
let sql = "DELETE FROM \(TABLE_MESSAGES) WHERE \(COLUMN_ID) IN (\(placeholders))" |
|||
|
|||
return executeUpdate(sql: sql, parameters: messageIds.map { $0 }) |
|||
} else if let sessionId = sessionId, !sessionId.isEmpty { |
|||
// 删除会话中的所有消息 |
|||
let sql = "DELETE FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" |
|||
return executeUpdate(sql: sql, parameters: [sessionId]) |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
/// 清空数据库 |
|||
/// - Returns: 成功返回true,失败返回false |
|||
func clearDatabase() -> Bool { |
|||
let sql = "DELETE FROM \(TABLE_MESSAGES)" |
|||
return executeUpdate(sql: sql) |
|||
} |
|||
|
|||
/// 获取指定会话的最近消息 |
|||
/// - Parameters: |
|||
/// - sessionId: 会话ID |
|||
/// - limit: 限制数量 |
|||
/// - Returns: 消息列表,按时间正序排列 |
|||
func getRecentMessages(sessionId: String, limit: Int) -> [[String: Any]] { |
|||
var messages: [[String: Any]] = [] |
|||
|
|||
// 查询语句 - 按时间倒序获取最近的N条,然后在结果中再按时间正序 |
|||
let sql = """ |
|||
SELECT * FROM ( |
|||
SELECT * FROM \(TABLE_MESSAGES) |
|||
WHERE \(COLUMN_SESSION_ID) = ? |
|||
ORDER BY \(COLUMN_TIMESTAMP) DESC |
|||
LIMIT ? |
|||
) tmp ORDER BY \(COLUMN_TIMESTAMP) ASC |
|||
""" |
|||
|
|||
executeQuery(sql: sql, parameters: [sessionId, limit]) { statement in |
|||
var message: [String: Any] = [:] |
|||
|
|||
// 获取ID |
|||
message["id"] = Int(sqlite3_column_int(statement, 0)) |
|||
|
|||
// 获取会话ID |
|||
if let sessionId = sqlite3_column_text(statement, 1) { |
|||
message["sessionId"] = String(cString: sessionId) |
|||
} |
|||
|
|||
// 获取消息内容 |
|||
if let messageText = sqlite3_column_text(statement, 2) { |
|||
message["message"] = String(cString: messageText) |
|||
} |
|||
|
|||
// 获取发送者 |
|||
if let sender = sqlite3_column_text(statement, 3) { |
|||
message["sender"] = String(cString: sender) |
|||
} |
|||
|
|||
// 获取时间戳 |
|||
if let timestamp = sqlite3_column_text(statement, 4) { |
|||
message["timestamp"] = String(cString: timestamp) |
|||
} |
|||
|
|||
// 获取元数据(如果有) |
|||
if sqlite3_column_type(statement, 5) != SQLITE_NULL { |
|||
if let metadata = sqlite3_column_text(statement, 5) { |
|||
message["metadata"] = String(cString: metadata) |
|||
} |
|||
} |
|||
|
|||
messages.append(message) |
|||
} |
|||
|
|||
return messages |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
#import <Flutter/Flutter.h> |
|||
|
|||
@interface ChatStoragePlugin : NSObject<FlutterPlugin> |
|||
@end |
|||
@ -0,0 +1,12 @@ |
|||
#import "ChatStoragePlugin.h" |
|||
#if __has_include(<chat_storage/chat_storage-Swift.h>) |
|||
#import <chat_storage/chat_storage-Swift.h> |
|||
#else |
|||
#import "chat_storage-Swift.h" |
|||
#endif |
|||
|
|||
@implementation ChatStoragePlugin |
|||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
|||
[SwiftChatStoragePlugin registerWithRegistrar:registrar]; |
|||
} |
|||
@end |
|||
@ -0,0 +1,182 @@ |
|||
import Flutter |
|||
import UIKit |
|||
|
|||
public class ChatStoragePlugin: NSObject, FlutterPlugin { |
|||
private let TAG = "ChatStoragePlugin" |
|||
private var chatStorageHelper: ChatStorageHelper? |
|||
|
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
let channel = FlutterMethodChannel(name: "chat_storage", binaryMessenger: registrar.messenger()) |
|||
let instance = ChatStoragePlugin() |
|||
registrar.addMethodCallDelegate(instance, channel: channel) |
|||
} |
|||
|
|||
override init() { |
|||
super.init() |
|||
chatStorageHelper = ChatStorageHelper(()) |
|||
print("\(TAG) 聊天存储插件已加载") |
|||
} |
|||
|
|||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
guard let helper = chatStorageHelper else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "存储助手未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
switch call.method { |
|||
case "saveMessage": |
|||
handleSaveMessage(call, result, helper) |
|||
|
|||
case "getMessages": |
|||
handleGetMessages(call, result, helper) |
|||
|
|||
case "getSessions": |
|||
handleGetSessions(call, result, helper) |
|||
|
|||
case "deleteMessages": |
|||
handleDeleteMessages(call, result, helper) |
|||
|
|||
case "clearDatabase": |
|||
handleClearDatabase(call, result, helper) |
|||
|
|||
case "getRecentMessages": |
|||
handleGetRecentMessages(call, result, helper) |
|||
|
|||
default: |
|||
result(FlutterMethodNotImplemented) |
|||
} |
|||
} |
|||
|
|||
// MARK: - Method handlers |
|||
|
|||
private func handleSaveMessage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
|||
guard let arguments = call.arguments as? [String: Any] else { |
|||
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) |
|||
return |
|||
} |
|||
|
|||
guard let sessionId = arguments["sessionId"] as? String, |
|||
let message = arguments["message"] as? String, |
|||
let sender = arguments["sender"] as? String else { |
|||
result(FlutterError(code: "MISSING_ARGS", message: "缺少必要参数", details: nil)) |
|||
return |
|||
} |
|||
|
|||
let metadata = arguments["metadata"] as? String |
|||
|
|||
do { |
|||
let id = helper.saveMessage(sessionId: sessionId, message: message, sender: sender, metadata: metadata) |
|||
|
|||
if id != -1 { |
|||
result(["success": true, "id": id]) |
|||
} else { |
|||
result(FlutterError(code: "DB_ERROR", message: "插入消息失败", details: nil)) |
|||
} |
|||
} catch { |
|||
print("\(TAG) 保存消息时发生错误: \(error.localizedDescription)") |
|||
result(FlutterError(code: "EXCEPTION", message: "保存消息时发生错误: \(error.localizedDescription)", details: nil)) |
|||
} |
|||
} |
|||
|
|||
private func handleGetMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
|||
guard let arguments = call.arguments as? [String: Any] else { |
|||
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) |
|||
return |
|||
} |
|||
|
|||
guard let sessionId = arguments["sessionId"] as? String else { |
|||
result(FlutterError(code: "MISSING_ARGS", message: "缺少会话ID", details: nil)) |
|||
return |
|||
} |
|||
|
|||
let page = arguments["page"] as? Int ?? 1 |
|||
let pageSize = arguments["pageSize"] as? Int ?? 20 |
|||
|
|||
do { |
|||
let messagesData = helper.getMessages(sessionId: sessionId, page: page, pageSize: pageSize) |
|||
|
|||
// 将结果转换为JSON字符串 |
|||
if let jsonData = try? JSONSerialization.data(withJSONObject: messagesData), |
|||
let jsonString = String(data: jsonData, encoding: .utf8) { |
|||
result(jsonString) |
|||
} else { |
|||
result(FlutterError(code: "JSON_ERROR", message: "无法序列化结果", details: nil)) |
|||
} |
|||
} catch { |
|||
print("\(TAG) 获取消息时发生错误: \(error.localizedDescription)") |
|||
result(FlutterError(code: "EXCEPTION", message: "获取消息时发生错误: \(error.localizedDescription)", details: nil)) |
|||
} |
|||
} |
|||
|
|||
private func handleGetSessions(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
|||
do { |
|||
let sessions = helper.getSessions() |
|||
|
|||
// 将结果转换为JSON字符串 |
|||
if let jsonData = try? JSONSerialization.data(withJSONObject: sessions), |
|||
let jsonString = String(data: jsonData, encoding: .utf8) { |
|||
result(jsonString) |
|||
} else { |
|||
result(FlutterError(code: "JSON_ERROR", message: "无法序列化结果", details: nil)) |
|||
} |
|||
} catch { |
|||
print("\(TAG) 获取会话列表时发生错误: \(error.localizedDescription)") |
|||
result(FlutterError(code: "EXCEPTION", message: "获取会话列表时发生错误: \(error.localizedDescription)", details: nil)) |
|||
} |
|||
} |
|||
|
|||
private func handleDeleteMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
|||
guard let arguments = call.arguments as? [String: Any] else { |
|||
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) |
|||
return |
|||
} |
|||
|
|||
let sessionId = arguments["sessionId"] as? String |
|||
let messageIds = arguments["messageIds"] as? [Int] |
|||
|
|||
do { |
|||
let success = helper.deleteMessages(sessionId: sessionId, messageIds: messageIds) |
|||
|
|||
if success { |
|||
result(["success": true]) |
|||
} else { |
|||
result(FlutterError(code: "INVALID_ARGS", message: "必须提供sessionId或messageIds", details: nil)) |
|||
} |
|||
} catch { |
|||
print("\(TAG) 删除消息时发生错误: \(error.localizedDescription)") |
|||
result(FlutterError(code: "EXCEPTION", message: "删除消息时发生错误: \(error.localizedDescription)", details: nil)) |
|||
} |
|||
} |
|||
|
|||
private func handleClearDatabase(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
|||
do { |
|||
let success = helper.clearDatabase() |
|||
result(["success": success]) |
|||
} catch { |
|||
print("\(TAG) 清空数据库时发生错误: \(error.localizedDescription)") |
|||
result(FlutterError(code: "EXCEPTION", message: "清空数据库时发生错误: \(error.localizedDescription)", details: nil)) |
|||
} |
|||
} |
|||
|
|||
private func handleGetRecentMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { |
|||
guard let arguments = call.arguments as? [String: Any] else { |
|||
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) |
|||
return |
|||
} |
|||
|
|||
guard let sessionId = arguments["sessionId"] as? String else { |
|||
result(FlutterError(code: "MISSING_ARGS", message: "缺少会话ID", details: nil)) |
|||
return |
|||
} |
|||
|
|||
let limit = arguments["limit"] as? Int ?? 10 |
|||
|
|||
do { |
|||
let messages = helper.getRecentMessages(sessionId: sessionId, limit: limit) |
|||
result(messages) |
|||
} catch { |
|||
print("\(TAG) 获取最近消息时发生错误: \(error.localizedDescription)") |
|||
result(FlutterError(code: "EXCEPTION", message: "获取最近消息时发生错误: \(error.localizedDescription)", details: nil)) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
import Flutter |
|||
import UIKit |
|||
|
|||
// 这个类是为了满足Flutter插件的命名约定 |
|||
// 实际实现在ChatStoragePlugin.swift中 |
|||
public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { |
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
// 调用实际实现类的注册方法 |
|||
ChatStoragePlugin.register(with: registrar) |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
# Chat Storage 插件 - iOS 实现 |
|||
|
|||
## 概述 |
|||
|
|||
Chat Storage 插件的 iOS 实现,为 Flutter 应用提供本地 SQLite 数据库支持,用于存储和管理聊天记录。 |
|||
|
|||
## 功能特性 |
|||
|
|||
- 基于 SQLite 的聊天记录存储 |
|||
- 支持按会话分组消息 |
|||
- 提供分页查询功能 |
|||
- 支持元数据存储 |
|||
- 完整的增删改查 API |
|||
|
|||
## 技术实现 |
|||
|
|||
本插件使用原生 Swift 实现,通过 SQLite C API 直接操作数据库,不依赖第三方 SQLite 封装库,以保持最小的依赖要求和最佳性能。 |
|||
|
|||
### 核心组件 |
|||
|
|||
- `ChatStorageHelper`: 负责 SQLite 数据库操作,提供完整的数据操作 API |
|||
- `ChatStoragePlugin`: 插件主类,处理与 Flutter 的通信 |
|||
|
|||
### 数据库结构 |
|||
|
|||
数据库包含一个 `messages` 表,结构如下: |
|||
|
|||
```sql |
|||
CREATE TABLE messages ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
session_id TEXT NOT NULL, |
|||
message TEXT NOT NULL, |
|||
sender TEXT NOT NULL, |
|||
timestamp TEXT NOT NULL, |
|||
metadata TEXT |
|||
); |
|||
|
|||
-- 索引 |
|||
CREATE INDEX idx_session_id ON messages (session_id); |
|||
CREATE INDEX idx_timestamp ON messages (timestamp); |
|||
``` |
|||
|
|||
## 使用注意事项 |
|||
|
|||
### SQLite 多线程访问 |
|||
|
|||
本插件所有数据库操作均在主线程执行,以避免 SQLite 多线程访问的复杂性。在处理大量数据时,建议在 Flutter 层面使用 `compute` 函数或 `Isolate` 进行异步处理。 |
|||
|
|||
### 数据格式 |
|||
|
|||
- 时间戳使用 `yyyy-MM-dd HH:mm:ss` 格式的字符串 |
|||
- JSON 转换遵循 Swift/Objective-C 的类型映射规则 |
|||
|
|||
## 调试与故障排除 |
|||
|
|||
- 日志前缀为 `ChatStoragePlugin` 和 `ChatStorageHelper` |
|||
- 可通过 Console.app 查看日志输出 |
|||
- 数据库文件存储在应用的 Documents/Databases 目录下 |
|||
|
|||
## 开发者须知 |
|||
|
|||
如果需要扩展此插件的功能,比如添加新的表或字段,应同时更新 Android 和 iOS 端的实现,并确保在版本升级时提供适当的数据库迁移策略。 |
|||
|
|||
数据库迁移逻辑目前实现为重建表,在生产环境中应该改为保留用户数据的方式。 |
|||
@ -0,0 +1,197 @@ |
|||
import Foundation |
|||
import ExternalAccessory |
|||
import CoreBluetooth |
|||
|
|||
/// 蓝牙事件类型 |
|||
enum BluetoothEventType { |
|||
case stateChanged |
|||
case deviceConnected |
|||
case deviceDisconnected |
|||
} |
|||
|
|||
/// 蓝牙状态枚举 |
|||
enum BluetoothState: String { |
|||
case unknown = "unknown" |
|||
case off = "off" |
|||
case on = "on" |
|||
case turningOn = "turningOn" |
|||
case turningOff = "turningOff" |
|||
} |
|||
|
|||
/// 经典蓝牙助手类,负责iOS上的蓝牙设备管理 |
|||
class ClassicBluetoothHelper: NSObject { |
|||
private let TAG = "ClassicBluetoothHelper" |
|||
|
|||
// 状态监控相关 |
|||
private var isMonitoring = false |
|||
private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)? |
|||
|
|||
// 用于监控蓝牙系统状态 |
|||
private var centralManager: CBCentralManager? |
|||
|
|||
// 通知观察者 |
|||
private var accessoryConnectionObserver: NSObjectProtocol? |
|||
private var accessoryDisconnectionObserver: NSObjectProtocol? |
|||
|
|||
override init() { |
|||
super.init() |
|||
centralManager = CBCentralManager(delegate: nil, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: false]) |
|||
} |
|||
|
|||
deinit { |
|||
stopBluetoothStateMonitoring() |
|||
} |
|||
|
|||
/// 检查蓝牙是否启用 |
|||
func isBluetoothEnabled() -> Bool { |
|||
guard let centralManager = centralManager else { return false } |
|||
return centralManager.state == .poweredOn |
|||
} |
|||
|
|||
/// 获取已连接的蓝牙设备 |
|||
/// - Parameter completion: 完成回调 |
|||
func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) { |
|||
// 获取所有已连接的外部配件 |
|||
let accessoryManager = EAAccessoryManager.shared() |
|||
let connectedAccessories = accessoryManager.connectedAccessories |
|||
|
|||
// 将配件转换为设备信息数组 |
|||
let devices = connectedAccessories.map { accessory -> [String: String] in |
|||
return [ |
|||
"name": accessory.name, |
|||
"address": accessory.serialNumber // iOS使用序列号作为唯一标识 |
|||
] |
|||
} |
|||
|
|||
// 返回结果 |
|||
completion(devices, nil) |
|||
} |
|||
|
|||
/// 开始监听蓝牙状态变化 |
|||
/// - Parameter callback: 状态变化回调 |
|||
func startBluetoothStateMonitoring(callback: @escaping (BluetoothEventType, [String: Any]) -> Void) { |
|||
self.stateCallback = callback |
|||
|
|||
if !isMonitoring { |
|||
isMonitoring = true |
|||
|
|||
// 设置蓝牙中央管理器代理 |
|||
centralManager?.delegate = self |
|||
|
|||
// 注册蓝牙配件连接/断开通知 |
|||
let notificationCenter = NotificationCenter.default |
|||
|
|||
// 观察配件连接 |
|||
accessoryConnectionObserver = notificationCenter.addObserver( |
|||
forName: NSNotification.Name.EAAccessoryDidConnect, |
|||
object: nil, |
|||
queue: nil |
|||
) { [weak self] notification in |
|||
guard let self = self, |
|||
let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else { |
|||
return |
|||
} |
|||
|
|||
let device: [String: String] = [ |
|||
"name": accessory.name, |
|||
"address": accessory.serialNumber |
|||
] |
|||
|
|||
self.stateCallback?(.deviceConnected, ["device": device]) |
|||
} |
|||
|
|||
// 观察配件断开 |
|||
accessoryDisconnectionObserver = notificationCenter.addObserver( |
|||
forName: NSNotification.Name.EAAccessoryDidDisconnect, |
|||
object: nil, |
|||
queue: nil |
|||
) { [weak self] notification in |
|||
guard let self = self, |
|||
let accessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else { |
|||
return |
|||
} |
|||
|
|||
let device: [String: String] = [ |
|||
"name": accessory.name, |
|||
"address": accessory.serialNumber |
|||
] |
|||
|
|||
self.stateCallback?(.deviceDisconnected, ["device": device]) |
|||
} |
|||
|
|||
// 开始监视配件变化 |
|||
EAAccessoryManager.shared().registerForLocalNotifications() |
|||
} |
|||
} |
|||
|
|||
/// 停止监听蓝牙状态变化 |
|||
func stopBluetoothStateMonitoring() { |
|||
if isMonitoring { |
|||
isMonitoring = false |
|||
|
|||
// 移除蓝牙中央管理器代理 |
|||
centralManager?.delegate = nil |
|||
|
|||
// 注销配件通知观察者 |
|||
if let observer = accessoryConnectionObserver { |
|||
NotificationCenter.default.removeObserver(observer) |
|||
accessoryConnectionObserver = nil |
|||
} |
|||
|
|||
if let observer = accessoryDisconnectionObserver { |
|||
NotificationCenter.default.removeObserver(observer) |
|||
accessoryDisconnectionObserver = nil |
|||
} |
|||
|
|||
// 停止监视配件变化 |
|||
EAAccessoryManager.shared().unregisterForLocalNotifications() |
|||
} |
|||
|
|||
// 清除回调 |
|||
stateCallback = nil |
|||
} |
|||
|
|||
/// 报告当前蓝牙状态 |
|||
func reportCurrentBluetoothState() { |
|||
guard let centralManager = centralManager else { return } |
|||
|
|||
let state: BluetoothState |
|||
|
|||
switch centralManager.state { |
|||
case .poweredOn: |
|||
state = .on |
|||
case .poweredOff: |
|||
state = .off |
|||
case .resetting: |
|||
state = .turningOff // 最接近的近似值 |
|||
case .unauthorized, .unsupported: |
|||
state = .off // 功能上等同于关闭 |
|||
default: |
|||
state = .unknown |
|||
} |
|||
|
|||
stateCallback?(.stateChanged, ["state": state.rawValue]) |
|||
} |
|||
} |
|||
|
|||
// MARK: - CBCentralManagerDelegate |
|||
extension ClassicBluetoothHelper: CBCentralManagerDelegate { |
|||
func centralManagerDidUpdateState(_ central: CBCentralManager) { |
|||
let state: BluetoothState |
|||
|
|||
switch central.state { |
|||
case .poweredOn: |
|||
state = .on |
|||
case .poweredOff: |
|||
state = .off |
|||
case .resetting: |
|||
state = .turningOff |
|||
case .unauthorized, .unsupported: |
|||
state = .off |
|||
default: |
|||
state = .unknown |
|||
} |
|||
|
|||
stateCallback?(.stateChanged, ["state": state.rawValue]) |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
#import <Flutter/Flutter.h> |
|||
|
|||
@interface ClassicBluetoothPlugin : NSObject<FlutterPlugin> |
|||
@end |
|||
@ -0,0 +1,12 @@ |
|||
#import "ClassicBluetoothPlugin.h" |
|||
#if __has_include(<classic_bluetooth/classic_bluetooth-Swift.h>) |
|||
#import <classic_bluetooth/classic_bluetooth-Swift.h> |
|||
#else |
|||
#import "classic_bluetooth-Swift.h" |
|||
#endif |
|||
|
|||
@implementation ClassicBluetoothPlugin |
|||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
|||
[SwiftClassicBluetoothPlugin registerWithRegistrar:registrar]; |
|||
} |
|||
@end |
|||
@ -0,0 +1,108 @@ |
|||
import Flutter |
|||
import UIKit |
|||
import ExternalAccessory |
|||
|
|||
public class ClassicBluetoothPlugin: NSObject, FlutterPlugin { |
|||
private let TAG = "ClassicBluetoothPlugin" |
|||
private var methodChannel: FlutterMethodChannel? |
|||
private var eventChannel: FlutterEventChannel? |
|||
private var eventSink: FlutterEventSink? |
|||
private let bluetoothHelper = ClassicBluetoothHelper() |
|||
|
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.classic_bluetooth/methods", binaryMessenger: registrar.messenger()) |
|||
let instance = ClassicBluetoothPlugin() |
|||
registrar.addMethodCallDelegate(instance, channel: channel) |
|||
|
|||
// 设置事件通道(用于蓝牙状态监控) |
|||
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.classic_bluetooth/events", binaryMessenger: registrar.messenger()) |
|||
eventChannel.setStreamHandler(instance) |
|||
|
|||
instance.methodChannel = channel |
|||
instance.eventChannel = eventChannel |
|||
} |
|||
|
|||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
switch call.method { |
|||
case "getConnectedDevices": |
|||
getConnectedDevices(result: result) |
|||
case "isBluetoothEnabled": |
|||
result(bluetoothHelper.isBluetoothEnabled()) |
|||
default: |
|||
result(FlutterMethodNotImplemented) |
|||
} |
|||
} |
|||
|
|||
private func getConnectedDevices(result: @escaping FlutterResult) { |
|||
if !bluetoothHelper.isBluetoothEnabled() { |
|||
result([]) |
|||
return |
|||
} |
|||
|
|||
bluetoothHelper.getConnectedDevices { devices, error in |
|||
if let error = error { |
|||
result(FlutterError(code: "BLUETOOTH_ERROR", message: error.localizedDescription, details: nil)) |
|||
return |
|||
} |
|||
|
|||
result(devices) |
|||
} |
|||
} |
|||
|
|||
// 发送事件到Flutter |
|||
private func sendEvent(_ event: [String: Any]) { |
|||
if let eventSink = eventSink { |
|||
eventSink(event) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - FlutterStreamHandler |
|||
extension ClassicBluetoothPlugin: FlutterStreamHandler { |
|||
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
|||
self.eventSink = events |
|||
|
|||
// 开始监听蓝牙状态变化 |
|||
bluetoothHelper.startBluetoothStateMonitoring { [weak self] eventType, data in |
|||
guard let self = self else { return } |
|||
|
|||
switch eventType { |
|||
case .stateChanged: |
|||
if let state = data["state"] as? String { |
|||
self.sendEvent([ |
|||
"type": "stateChanged", |
|||
"state": state, |
|||
"timestamp": Int(Date().timeIntervalSince1970 * 1000) |
|||
]) |
|||
} |
|||
case .deviceConnected: |
|||
if let device = data["device"] as? [String: String] { |
|||
self.sendEvent([ |
|||
"type": "deviceConnected", |
|||
"device": device, |
|||
"timestamp": Int(Date().timeIntervalSince1970 * 1000) |
|||
]) |
|||
} |
|||
case .deviceDisconnected: |
|||
if let device = data["device"] as? [String: String] { |
|||
self.sendEvent([ |
|||
"type": "deviceDisconnected", |
|||
"device": device, |
|||
"timestamp": Int(Date().timeIntervalSince1970 * 1000) |
|||
]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 报告当前蓝牙状态 |
|||
bluetoothHelper.reportCurrentBluetoothState() |
|||
|
|||
return nil |
|||
} |
|||
|
|||
public func onCancel(withArguments arguments: Any?) -> FlutterError? { |
|||
eventSink = nil |
|||
bluetoothHelper.stopBluetoothStateMonitoring() |
|||
return nil |
|||
} |
|||
} |
|||
@ -0,0 +1,136 @@ |
|||
import Foundation |
|||
import os.log |
|||
|
|||
/// iOS端的文件日志助手类,提供与Android端FileLogger类似的功能 |
|||
class FileLogger { |
|||
|
|||
private static let logQueue = DispatchQueue(label: "com.yunqiinnovation.classic_bluetooth.logger", qos: .background) |
|||
private static let dateFormatter: DateFormatter = { |
|||
let formatter = DateFormatter() |
|||
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" |
|||
return formatter |
|||
}() |
|||
|
|||
/// 日志类型 |
|||
enum LogLevel: String { |
|||
case debug = "DEBUG" |
|||
case info = "INFO" |
|||
case warning = "WARNING" |
|||
case error = "ERROR" |
|||
} |
|||
|
|||
/// 输出调试日志 |
|||
/// - Parameters: |
|||
/// - tag: 标签 |
|||
/// - message: 日志消息 |
|||
static func d(_ tag: String, _ message: String) { |
|||
log(level: .debug, tag: tag, message: message) |
|||
} |
|||
|
|||
/// 输出信息日志 |
|||
/// - Parameters: |
|||
/// - tag: 标签 |
|||
/// - message: 日志消息 |
|||
static func i(_ tag: String, _ message: String) { |
|||
log(level: .info, tag: tag, message: message) |
|||
} |
|||
|
|||
/// 输出警告日志 |
|||
/// - Parameters: |
|||
/// - tag: 标签 |
|||
/// - message: 日志消息 |
|||
static func w(_ tag: String, _ message: String) { |
|||
log(level: .warning, tag: tag, message: message) |
|||
} |
|||
|
|||
/// 输出错误日志 |
|||
/// - Parameters: |
|||
/// - tag: 标签 |
|||
/// - message: 日志消息 |
|||
/// - error: 可选的错误对象 |
|||
static func e(_ tag: String, _ message: String, _ error: Error? = nil) { |
|||
var fullMessage = message |
|||
if let error = error { |
|||
fullMessage += " - Error: \(error.localizedDescription)" |
|||
} |
|||
log(level: .error, tag: tag, message: fullMessage) |
|||
} |
|||
|
|||
/// 输出日志 |
|||
/// - Parameters: |
|||
/// - level: 日志级别 |
|||
/// - tag: 标签 |
|||
/// - message: 日志消息 |
|||
private static func log(level: LogLevel, tag: String, message: String) { |
|||
let timestamp = dateFormatter.string(from: Date()) |
|||
let logMessage = "[\(timestamp)] [\(level.rawValue)] [\(tag)]: \(message)" |
|||
|
|||
// 输出到控制台 |
|||
print(logMessage) |
|||
|
|||
// 异步写入日志文件 |
|||
logQueue.async { |
|||
writeToLogFile(logMessage) |
|||
} |
|||
|
|||
// 使用系统日志 |
|||
if #available(iOS 10.0, *) { |
|||
let osLog = OSLog(subsystem: "com.yunqiinnovation.classic_bluetooth", category: tag) |
|||
let osLogType: OSLogType |
|||
|
|||
switch level { |
|||
case .debug: |
|||
osLogType = .debug |
|||
case .info: |
|||
osLogType = .info |
|||
case .warning: |
|||
osLogType = .default |
|||
case .error: |
|||
osLogType = .error |
|||
} |
|||
|
|||
os_log("%{public}@", log: osLog, type: osLogType, message) |
|||
} |
|||
} |
|||
|
|||
/// 将日志写入文件 |
|||
/// - Parameter message: 日志消息 |
|||
private static func writeToLogFile(_ message: String) { |
|||
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { |
|||
return |
|||
} |
|||
|
|||
let logDirectoryURL = documentsDirectory.appendingPathComponent("Logs") |
|||
let dateString = DateFormatter().with { $0.dateFormat = "yyyy-MM-dd" }.string(from: Date()) |
|||
let logFileURL = logDirectoryURL.appendingPathComponent("bluetooth_\(dateString).log") |
|||
|
|||
do { |
|||
// 创建日志文件夹 |
|||
try FileManager.default.createDirectory(at: logDirectoryURL, withIntermediateDirectories: true) |
|||
|
|||
// 如果文件不存在,创建文件 |
|||
if !FileManager.default.fileExists(atPath: logFileURL.path) { |
|||
FileManager.default.createFile(atPath: logFileURL.path, contents: nil) |
|||
} |
|||
|
|||
// 追加日志内容 |
|||
if let fileHandle = FileHandle(forWritingAtPath: logFileURL.path) { |
|||
fileHandle.seekToEndOfFile() |
|||
if let data = "\(message)\n".data(using: .utf8) { |
|||
fileHandle.write(data) |
|||
} |
|||
fileHandle.closeFile() |
|||
} |
|||
} catch { |
|||
print("写入日志文件失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - DateFormatter 扩展 |
|||
extension DateFormatter { |
|||
func with(_ block: (DateFormatter) -> Void) -> DateFormatter { |
|||
block(self) |
|||
return self |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
import Flutter |
|||
import UIKit |
|||
|
|||
// 这个文件是为了满足Flutter插件的命名约定 |
|||
// 实际实现在ClassicBluetoothPlugin.swift中 |
|||
public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin { |
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
// 调用真正的实现 |
|||
ClassicBluetoothPlugin.register(with: registrar) |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
# iOS蓝牙配置说明 |
|||
|
|||
## 所需权限 |
|||
|
|||
要在iOS中使用经典蓝牙功能,需要在应用的`Info.plist`中添加以下配置: |
|||
|
|||
### 1. 外部附件协议支持 |
|||
|
|||
在Info.plist中添加`UISupportedExternalAccessoryProtocols`数组,并添加你的设备支持的协议。如果不确定,可以先添加常见的协议: |
|||
|
|||
```xml |
|||
<key>UISupportedExternalAccessoryProtocols</key> |
|||
<array> |
|||
<string>com.example.protocol</string> |
|||
<!-- 添加其他你设备支持的协议 --> |
|||
</array> |
|||
``` |
|||
|
|||
### 2. 蓝牙使用说明 |
|||
|
|||
在iOS 13及以上版本中,需要添加蓝牙使用说明: |
|||
|
|||
```xml |
|||
<key>NSBluetoothAlwaysUsageDescription</key> |
|||
<string>我们需要使用蓝牙来连接您的音频设备</string> |
|||
``` |
|||
|
|||
对于iOS 13以下版本: |
|||
|
|||
```xml |
|||
<key>NSBluetoothPeripheralUsageDescription</key> |
|||
<string>我们需要使用蓝牙来连接您的音频设备</string> |
|||
``` |
|||
|
|||
## 常见问题 |
|||
|
|||
1. 如果看不到任何设备,请确保: |
|||
- 设备已配对并连接到iOS设备 |
|||
- 设备是MFi认证的(Made for iPhone/iPad),因为iOS对经典蓝牙有限制 |
|||
- 添加了正确的协议字符串 |
|||
|
|||
2. 蓝牙状态变化不及时: |
|||
- iOS系统对蓝牙状态变化的通知可能有延迟 |
|||
- 请确保用户已经授权蓝牙权限 |
|||
@ -0,0 +1,22 @@ |
|||
# |
|||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
|||
# |
|||
Pod::Spec.new do |s| |
|||
s.name = 'classic_bluetooth' |
|||
s.version = '0.0.1' |
|||
s.summary = '经典蓝牙插件' |
|||
s.description = <<-DESC |
|||
一个用于管理经典蓝牙设备的Flutter插件。 |
|||
DESC |
|||
s.homepage = 'http://yunqiinnovation.com' |
|||
s.license = { :file => '../LICENSE' } |
|||
s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' } |
|||
s.source = { :path => '.' } |
|||
s.source_files = 'Classes/**/*' |
|||
|
|||
s.ios.deployment_target = '11.0' |
|||
s.swift_version = '5.0' |
|||
|
|||
# 添加CoreBluetooth和ExternalAccessory框架依赖 |
|||
s.framework = 'CoreBluetooth', 'ExternalAccessory' |
|||
end |
|||
@ -0,0 +1,4 @@ |
|||
#import <Flutter/Flutter.h> |
|||
|
|||
@interface DeepvoiceUtilsPlugin : NSObject<FlutterPlugin> |
|||
@end |
|||
@ -0,0 +1,12 @@ |
|||
#import "DeepvoiceUtilsPlugin.h" |
|||
#if __has_include(<deepvoice_utils/deepvoice_utils-Swift.h>) |
|||
#import <deepvoice_utils/deepvoice_utils-Swift.h> |
|||
#else |
|||
#import "deepvoice_utils-Swift.h" |
|||
#endif |
|||
|
|||
@implementation DeepvoiceUtilsPlugin |
|||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
|||
[SwiftDeepvoiceUtilsPlugin registerWithRegistrar:registrar]; |
|||
} |
|||
@end |
|||
@ -0,0 +1,30 @@ |
|||
import Flutter |
|||
import UIKit |
|||
|
|||
public class DeepvoiceUtilsPlugin: NSObject, FlutterPlugin { |
|||
private static let TAG = "DeepvoiceUtilsPlugin" |
|||
|
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
let channel = FlutterMethodChannel(name: "deepvoice_utils", binaryMessenger: registrar.messenger()) |
|||
let instance = DeepvoiceUtilsPlugin() |
|||
registrar.addMethodCallDelegate(instance, channel: channel) |
|||
|
|||
// 初始化文件日志 |
|||
FileLogger.initialize() |
|||
FileLogger.d(TAG, "DeepvoiceUtilsPlugin 已初始化") |
|||
} |
|||
|
|||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
switch call.method { |
|||
case "getPlatformVersion": |
|||
result("iOS \(UIDevice.current.systemVersion)") |
|||
default: |
|||
result(FlutterMethodNotImplemented) |
|||
} |
|||
} |
|||
|
|||
public func detachFromEngine(for registrar: FlutterPluginRegistrar) { |
|||
// 关闭日志系统 |
|||
FileLogger.shutdown() |
|||
} |
|||
} |
|||
@ -0,0 +1,286 @@ |
|||
import Foundation |
|||
import os.log |
|||
|
|||
/** |
|||
* 文件日志工具 |
|||
* 将日志同时输出到控制台和文件系统 |
|||
*/ |
|||
@objc public class FileLogger: NSObject { |
|||
|
|||
private static let TAG = "FileLogger" |
|||
private static var instance: FileLogger? |
|||
|
|||
private let logDir: URL |
|||
private var currentLogFile: URL? |
|||
private let logQueue = DispatchQueue(label: "com.yunqiinnovation.deepvoice_utils.logger", qos: .utility) |
|||
|
|||
private let dateFormatter: DateFormatter = { |
|||
let formatter = DateFormatter() |
|||
formatter.dateFormat = "yyyy-MM-dd" |
|||
return formatter |
|||
}() |
|||
|
|||
private let timeFormatter: DateFormatter = { |
|||
let formatter = DateFormatter() |
|||
formatter.dateFormat = "HH:mm:ss.SSS" |
|||
return formatter |
|||
}() |
|||
|
|||
/** |
|||
* 初始化日志系统 |
|||
*/ |
|||
private init(directory: URL) { |
|||
self.logDir = directory.appendingPathComponent("logs", isDirectory: true) |
|||
super.init() |
|||
|
|||
// 创建日志目录 |
|||
try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true) |
|||
|
|||
// 创建当天日志文件 |
|||
updateLogFile() |
|||
|
|||
// 记录初始化完成 |
|||
self.d(FileLogger.TAG, "文件日志系统已初始化") |
|||
} |
|||
|
|||
/** |
|||
* 更新日志文件为当天日期 |
|||
*/ |
|||
private func updateLogFile() { |
|||
let today = dateFormatter.string(from: Date()) |
|||
let logFile = logDir.appendingPathComponent("log_\(today).txt") |
|||
|
|||
if currentLogFile == nil || currentLogFile?.path != logFile.path { |
|||
currentLogFile = logFile |
|||
|
|||
// 如果是新文件,添加文件头 |
|||
if !FileManager.default.fileExists(atPath: logFile.path) { |
|||
let headerDate = DateFormatter() |
|||
headerDate.dateFormat = "yyyy-MM-dd HH:mm:ss" |
|||
let headerLine = "===== 日志开始: \(headerDate.string(from: Date())) =====\n" |
|||
|
|||
try? headerLine.write(to: logFile, atomically: true, encoding: .utf8) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将日志写入文件 |
|||
*/ |
|||
private func logToFile(level: String, tag: String, message: String) { |
|||
logQueue.async { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
do { |
|||
// 检查是否需要更新日志文件 |
|||
self.updateLogFile() |
|||
|
|||
guard let logFile = self.currentLogFile, |
|||
FileManager.default.fileExists(atPath: logFile.path) else { |
|||
return |
|||
} |
|||
|
|||
// 格式化日志内容 |
|||
let timestamp = self.timeFormatter.string(from: Date()) |
|||
let logLine = "\(timestamp) \(level)/\(tag): \(message)\n" |
|||
|
|||
// 获取文件句柄并追加内容 |
|||
if let fileHandle = try? FileHandle(forWritingTo: logFile) { |
|||
fileHandle.seekToEndOfFile() |
|||
if let data = logLine.data(using: .utf8) { |
|||
fileHandle.write(data) |
|||
} |
|||
fileHandle.closeFile() |
|||
} else { |
|||
// 如果无法获取文件句柄,尝试直接写入 |
|||
let existingContent = try? String(contentsOf: logFile, encoding: .utf8) |
|||
let newContent = (existingContent ?? "") + logLine |
|||
try newContent.write(to: logFile, atomically: true, encoding: .utf8) |
|||
} |
|||
} catch { |
|||
print("写入日志文件失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 调试级别日志 |
|||
*/ |
|||
private func d(_ tag: String, _ message: String) { |
|||
// 输出到控制台 |
|||
print("D/\(tag): \(message)") |
|||
|
|||
// 写入文件 |
|||
logToFile(level: "D", tag: tag, message: message) |
|||
} |
|||
|
|||
/** |
|||
* 信息级别日志 |
|||
*/ |
|||
private func i(_ tag: String, _ message: String) { |
|||
// 输出到控制台 |
|||
print("I/\(tag): \(message)") |
|||
|
|||
// 写入文件 |
|||
logToFile(level: "I", tag: tag, message: message) |
|||
} |
|||
|
|||
/** |
|||
* 警告级别日志 |
|||
*/ |
|||
private func w(_ tag: String, _ message: String) { |
|||
// 输出到控制台 |
|||
print("W/\(tag): \(message)") |
|||
|
|||
// 写入文件 |
|||
logToFile(level: "W", tag: tag, message: message) |
|||
} |
|||
|
|||
/** |
|||
* 错误级别日志 |
|||
*/ |
|||
private func e(_ tag: String, _ message: String, _ error: Error? = nil) { |
|||
var fullMessage = message |
|||
|
|||
// 如果有错误,添加错误信息 |
|||
if let error = error { |
|||
fullMessage += "\n\(error.localizedDescription)" |
|||
} |
|||
|
|||
// 输出到控制台 |
|||
print("E/\(tag): \(fullMessage)") |
|||
|
|||
// 写入文件 |
|||
logToFile(level: "E", tag: tag, message: fullMessage) |
|||
} |
|||
|
|||
/** |
|||
* 清理旧日志文件 |
|||
*/ |
|||
private func cleanOldLogs(daysToKeep: Int = 7) { |
|||
logQueue.async { |
|||
do { |
|||
let currentDate = Date() |
|||
let fileManager = FileManager.default |
|||
|
|||
let logFiles = try fileManager.contentsOfDirectory(at: self.logDir, includingPropertiesForKeys: [.creationDateKey], options: []) |
|||
|
|||
for fileURL in logFiles where fileURL.pathExtension == "txt" { |
|||
if let fileAttributes = try? fileManager.attributesOfItem(atPath: fileURL.path), |
|||
let creationDate = fileAttributes[.creationDate] as? Date { |
|||
|
|||
let age = Calendar.current.dateComponents([.day], from: creationDate, to: currentDate) |
|||
|
|||
if let days = age.day, days > daysToKeep { |
|||
try? fileManager.removeItem(at: fileURL) |
|||
print("D/\(FileLogger.TAG): 已删除旧日志文件: \(fileURL.lastPathComponent)") |
|||
} |
|||
} |
|||
} |
|||
} catch { |
|||
print("E/\(FileLogger.TAG): 清理旧日志失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - 公共静态方法 |
|||
|
|||
/** |
|||
* 初始化日志系统 |
|||
*/ |
|||
@objc public static func initialize() { |
|||
if instance == nil { |
|||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] |
|||
instance = FileLogger(directory: documentsDirectory) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 初始化日志系统(使用自定义目录) |
|||
*/ |
|||
@objc public static func initialize(withDirectory directory: URL) { |
|||
if instance == nil { |
|||
instance = FileLogger(directory: directory) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 调试日志 |
|||
*/ |
|||
@objc public static func d(_ tag: String, _ message: String) { |
|||
// 如果实例不存在,先使用默认目录初始化 |
|||
if instance == nil { |
|||
initialize() |
|||
} |
|||
|
|||
instance?.d(tag, message) |
|||
|
|||
// 使用系统日志 |
|||
if #available(iOS 10.0, *) { |
|||
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .debug, message) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 信息日志 |
|||
*/ |
|||
@objc public static func i(_ tag: String, _ message: String) { |
|||
if instance == nil { |
|||
initialize() |
|||
} |
|||
|
|||
instance?.i(tag, message) |
|||
|
|||
// 使用系统日志 |
|||
if #available(iOS 10.0, *) { |
|||
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .info, message) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 警告日志 |
|||
*/ |
|||
@objc public static func w(_ tag: String, _ message: String) { |
|||
if instance == nil { |
|||
initialize() |
|||
} |
|||
|
|||
instance?.w(tag, message) |
|||
|
|||
// 使用系统日志 |
|||
if #available(iOS 10.0, *) { |
|||
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .default, message) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 错误日志 |
|||
*/ |
|||
@objc public static func e(_ tag: String, _ message: String, _ error: Error? = nil) { |
|||
if instance == nil { |
|||
initialize() |
|||
} |
|||
|
|||
instance?.e(tag, message, error) |
|||
|
|||
// 使用系统日志 |
|||
if #available(iOS 10.0, *) { |
|||
let errorMessage = error != nil ? "\(message): \(error!.localizedDescription)" : message |
|||
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .error, errorMessage) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清理旧日志 |
|||
*/ |
|||
@objc public static func cleanOldLogs(daysToKeep: Int = 7) { |
|||
instance?.cleanOldLogs(daysToKeep: daysToKeep) |
|||
} |
|||
|
|||
/** |
|||
* 关闭日志系统 |
|||
*/ |
|||
@objc public static func shutdown() { |
|||
instance = nil |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
import Flutter |
|||
import UIKit |
|||
|
|||
// 这个类是为了满足Flutter插件的命名约定 |
|||
// 实际实现在DeepvoiceUtilsPlugin.swift中 |
|||
public class SwiftDeepvoiceUtilsPlugin: NSObject, FlutterPlugin { |
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
// 调用真正的实现 |
|||
DeepvoiceUtilsPlugin.register(with: registrar) |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
# DeepVoice Utils iOS 实现 |
|||
|
|||
## 简介 |
|||
|
|||
本模块是 `deepvoice_utils` 插件的 iOS 实现部分,主要提供以下功能: |
|||
|
|||
- 文件日志记录(FileLogger):将日志记录到文件,方便调试和问题排查 |
|||
|
|||
## 使用说明 |
|||
|
|||
### 日志系统 |
|||
|
|||
该插件自动初始化日志系统,开发者无需手动调用初始化方法。日志文件默认保存在应用的 Documents 目录下的 logs 文件夹中,按日期分文件存储。 |
|||
|
|||
## 配置要求 |
|||
|
|||
- iOS 11.0 或更高版本 |
|||
- Swift 5.0 |
|||
- 确保应用拥有文件写入权限 |
|||
|
|||
## 开发说明 |
|||
|
|||
### 文件结构 |
|||
|
|||
- `DeepvoiceUtilsPlugin.swift`:插件主类,负责与 Flutter 交互 |
|||
- `FileLogger.swift`:日志系统实现,提供日志记录功能 |
|||
- `SwiftDeepvoiceUtilsPlugin.swift`:命名约定桥接类 |
|||
- `DeepvoiceUtilsPlugin.h/.m`:Objective-C 桥接文件 |
|||
|
|||
### 自定义日志目录 |
|||
|
|||
如需使用自定义日志目录,可以在应用启动时手动调用初始化方法: |
|||
|
|||
```swift |
|||
import deepvoice_utils |
|||
|
|||
// 在 AppDelegate 中 |
|||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { |
|||
// 自定义日志目录 |
|||
let customDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("custom_logs") |
|||
FileLogger.initialize(withDirectory: customDirectory) |
|||
|
|||
return true |
|||
} |
|||
``` |
|||
|
|||
### 清理旧日志 |
|||
|
|||
默认情况下,日志系统不会自动清理旧日志。如果需要清理,可以手动调用: |
|||
|
|||
```swift |
|||
// 清理7天前的日志 |
|||
FileLogger.cleanOldLogs(daysToKeep: 7) |
|||
``` |
|||
@ -0,0 +1,19 @@ |
|||
# |
|||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
|||
# |
|||
Pod::Spec.new do |s| |
|||
s.name = 'deepvoice_utils' |
|||
s.version = '0.0.1' |
|||
s.summary = '深语工具库' |
|||
s.description = <<-DESC |
|||
提供文件日志记录等通用功能的Flutter插件。 |
|||
DESC |
|||
s.homepage = 'http://yunqiinnovation.com' |
|||
s.license = { :file => '../LICENSE' } |
|||
s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' } |
|||
s.source = { :path => '.' } |
|||
s.source_files = 'Classes/**/*' |
|||
|
|||
s.ios.deployment_target = '11.0' |
|||
s.swift_version = '5.0' |
|||
end |
|||
@ -0,0 +1,33 @@ |
|||
# 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/ |
|||
@ -0,0 +1,33 @@ |
|||
# 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: "c23637390482d4cf9598c3ce3f2be31aa7332daf" |
|||
channel: "stable" |
|||
|
|||
project_type: plugin |
|||
|
|||
# Tracks metadata for the flutter migrate command |
|||
migration: |
|||
platforms: |
|||
- platform: root |
|||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf |
|||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf |
|||
- platform: android |
|||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf |
|||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf |
|||
- platform: ios |
|||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf |
|||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf |
|||
|
|||
# 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' |
|||
@ -0,0 +1,3 @@ |
|||
## 0.0.1 |
|||
|
|||
* TODO: Describe initial release. |
|||
@ -0,0 +1 @@ |
|||
TODO: Add your license here. |
|||
@ -0,0 +1,15 @@ |
|||
# music_service |
|||
|
|||
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. |
|||
|
|||
@ -0,0 +1,4 @@ |
|||
include: package:flutter_lints/flutter.yaml |
|||
|
|||
# Additional information about this file can be found at |
|||
# https://dart.dev/guides/language/analysis-options |
|||
@ -0,0 +1,9 @@ |
|||
*.iml |
|||
.gradle |
|||
/local.properties |
|||
/.idea/workspace.xml |
|||
/.idea/libraries |
|||
.DS_Store |
|||
/build |
|||
/captures |
|||
.cxx |
|||
@ -0,0 +1,70 @@ |
|||
group = "com.yunqiinnovation.music_service" |
|||
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.music_service" |
|||
|
|||
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 { |
|||
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 |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
dependencies { |
|||
implementation project(':deepvoice_utils') |
|||
} |
|||
@ -0,0 +1 @@ |
|||
rootProject.name = 'music_service' |
|||
@ -0,0 +1,11 @@ |
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
|||
package="com.yunqiinnovation.music_service"> |
|||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> |
|||
<application> |
|||
<service |
|||
android:name=".MusicService" |
|||
android:exported="false" |
|||
android:enabled="true" |
|||
android:foregroundServiceType="mediaPlayback" /> |
|||
</application> |
|||
</manifest> |
|||
@ -0,0 +1,153 @@ |
|||
package com.yunqiinnovation.music_service |
|||
|
|||
import android.app.Notification |
|||
import android.app.NotificationChannel |
|||
import android.app.NotificationManager |
|||
import android.app.PendingIntent |
|||
import android.app.Service |
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import android.media.AudioManager |
|||
import android.media.MediaPlayer |
|||
import android.os.Build |
|||
import android.os.IBinder |
|||
import android.util.Log |
|||
import androidx.core.app.NotificationCompat |
|||
import com.yunqiinnovation.music_service.MusicServiceStarter |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import java.util.concurrent.atomic.AtomicBoolean |
|||
|
|||
class MusicService : Service() { |
|||
|
|||
companion object { |
|||
private const val TAG = "MusicService" |
|||
private const val CHANNEL_ID = "music_media_service_channel" |
|||
private const val NOTIFICATION_ID = 1002 |
|||
private const val PREF_NAME = "deep_voice_config" |
|||
|
|||
// 用于跟踪服务是否正在运行 |
|||
private val isRunning = AtomicBoolean(false) |
|||
|
|||
// 获取服务运行状态 |
|||
fun isServiceRunning(): Boolean = isRunning.get() |
|||
} |
|||
private var mediaPlayer: MediaPlayer? = null |
|||
private val playlist = mutableListOf<String>() // 可扩展支持播放列表 |
|||
private var currentIndex = 0 |
|||
|
|||
override fun onCreate() { |
|||
super.onCreate() |
|||
val notification = createNotification() // 创建一个非空通知 |
|||
startForeground(1, notification) |
|||
} |
|||
private var isPlaying = false |
|||
/** |
|||
* 创建前台服务通知 |
|||
*/ |
|||
private fun createNotification(): Notification { |
|||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager |
|||
|
|||
// Android 8.0+ 需要创建通知渠道 |
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
|||
val channel = NotificationChannel( |
|||
CHANNEL_ID, |
|||
"音乐播放服务", |
|||
NotificationManager.IMPORTANCE_LOW |
|||
) |
|||
manager.createNotificationChannel(channel) |
|||
} |
|||
|
|||
// 点击通知打开主界面 |
|||
val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply { |
|||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP |
|||
} |
|||
val pendingIntent = if (intent != null) { |
|||
PendingIntent.getActivity( |
|||
this, |
|||
0, |
|||
intent, |
|||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT |
|||
) |
|||
} else { |
|||
null |
|||
} |
|||
|
|||
// 构建通知 |
|||
val builder = NotificationCompat.Builder(this, CHANNEL_ID) |
|||
.setContentTitle("DeepSound") |
|||
.setContentText(if (isPlaying) "正在播放" else "已暂停") |
|||
.setSmallIcon(android.R.drawable.ic_media_play) |
|||
|
|||
// 设置点击意图 |
|||
pendingIntent?.let { builder.setContentIntent(it) } |
|||
|
|||
return builder.build() |
|||
} |
|||
|
|||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { |
|||
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY |
|||
Log.i(TAG, "onStartCommand: $command") |
|||
when (command) { |
|||
"play" -> { |
|||
val url = intent.getStringExtra("url") ?: return START_NOT_STICKY |
|||
play(url) |
|||
} |
|||
"pause" -> mediaPlayer?.pause() |
|||
"resume" -> mediaPlayer?.start() |
|||
"stop" -> stopSelf() |
|||
"next" -> { |
|||
currentIndex++ |
|||
if (currentIndex < playlist.size) play(playlist[currentIndex]) |
|||
} |
|||
"previous" -> { |
|||
currentIndex-- |
|||
if (currentIndex >= 0) play(playlist[currentIndex]) |
|||
} |
|||
"setVolume" -> { |
|||
val volume = intent.getFloatExtra("volume", 1.0f) |
|||
mediaPlayer?.setVolume(volume, volume) |
|||
} |
|||
} |
|||
return START_STICKY |
|||
} |
|||
|
|||
private fun play(url: String) { |
|||
Log.i(TAG, "开始播放音乐: $url") |
|||
mediaPlayer?.release() |
|||
mediaPlayer = MediaPlayer().apply { |
|||
setDataSource(url) |
|||
setAudioStreamType(AudioManager.STREAM_MUSIC) |
|||
prepareAsync() |
|||
setOnPreparedListener { start() } |
|||
setOnCompletionListener { stopSelf() } |
|||
} |
|||
// startForeground(1, NotificationUtils.createNotification(this, "Playing...")) |
|||
} |
|||
|
|||
override fun onDestroy() { |
|||
mediaPlayer?.release() |
|||
stopForeground(true) |
|||
super.onDestroy() |
|||
} |
|||
|
|||
override fun onBind(intent: Intent?): IBinder? = null |
|||
|
|||
/** |
|||
* 释放资源 |
|||
*/ |
|||
fun release() { |
|||
try { |
|||
mediaPlayer?.apply { |
|||
if (isPlaying) stop() |
|||
release() |
|||
} |
|||
mediaPlayer = null |
|||
} catch (e: Exception) { |
|||
// 忽略异常 |
|||
} |
|||
} |
|||
/** |
|||
* 是否正在播放 |
|||
*/ |
|||
fun isPlaying() = mediaPlayer?.isPlaying == true |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
package com.yunqiinnovation.music_service |
|||
|
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import androidx.core.content.ContextCompat |
|||
import com.yunqiinnovation.deepvoice_utils.FileLogger |
|||
import io.flutter.embedding.engine.plugins.FlutterPlugin |
|||
import io.flutter.plugin.common.MethodCall |
|||
import io.flutter.plugin.common.MethodChannel |
|||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler |
|||
|
|||
|
|||
/** MusicServicePlugin */ |
|||
class MusicServicePlugin: FlutterPlugin, MethodCallHandler { |
|||
private val TAG = "MusicServicePlugin" |
|||
private lateinit var context: Context |
|||
private lateinit var channel : MethodChannel |
|||
|
|||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { |
|||
context = flutterPluginBinding.applicationContext |
|||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "music_service") |
|||
channel.setMethodCallHandler(this) |
|||
} |
|||
|
|||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { |
|||
FileLogger.d(TAG, "Method call: ${call.method}") |
|||
val intent = Intent(context, MusicService::class.java) |
|||
when (call.method) { |
|||
"play" -> { |
|||
val url = call.argument<String>("url") |
|||
MusicServiceStarter.startServiceWithCommand(context, "play", url) |
|||
} |
|||
"pause" -> MusicServiceStarter.startServiceWithCommand(context, "pause") |
|||
"resume" -> MusicServiceStarter.startServiceWithCommand(context, "resume") |
|||
"stop" -> MusicServiceStarter.startServiceWithCommand(context, "stop") |
|||
"setVolume" -> { |
|||
val volume = call.argument<Double>("volume")?.toFloat() |
|||
MusicServiceStarter.startServiceWithCommand(context, "setVolume", volume = volume) |
|||
} |
|||
else -> result.notImplemented() |
|||
} |
|||
result.success(null) |
|||
} |
|||
|
|||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
package com.yunqiinnovation.music_service |
|||
|
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import android.util.Log |
|||
import androidx.core.content.ContextCompat |
|||
|
|||
object MusicServiceStarter { |
|||
private val TAG = "MusicServiceStarter" |
|||
fun startServiceWithCommand(context: Context, command: String, url: String? = null, volume: Float? = null) { |
|||
Log.i(TAG, "startServiceWithCommand: $command $url") |
|||
val intent = Intent(context, MusicService::class.java).apply { |
|||
putExtra("command", command) |
|||
url?.let { putExtra("url", it) } |
|||
volume?.let { putExtra("volume", it) } |
|||
} |
|||
ContextCompat.startForegroundService(context, intent) |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
.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 |
|||
@ -0,0 +1,19 @@ |
|||
import Flutter |
|||
import UIKit |
|||
|
|||
public class MusicServicePlugin: NSObject, FlutterPlugin { |
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
let channel = FlutterMethodChannel(name: "music_service", binaryMessenger: registrar.messenger()) |
|||
let instance = MusicServicePlugin() |
|||
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) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|||
<plist version="1.0"> |
|||
<dict> |
|||
<key>NSPrivacyTrackingDomains</key> |
|||
<array/> |
|||
<key>NSPrivacyAccessedAPITypes</key> |
|||
<array/> |
|||
<key>NSPrivacyCollectedDataTypes</key> |
|||
<array/> |
|||
<key>NSPrivacyTracking</key> |
|||
<false/> |
|||
</dict> |
|||
</plist> |
|||
@ -0,0 +1,29 @@ |
|||
# |
|||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
|||
# Run `pod lib lint music_service.podspec` to validate before publishing. |
|||
# |
|||
Pod::Spec.new do |s| |
|||
s.name = 'music_service' |
|||
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 = {'music_service_privacy' => ['Resources/PrivacyInfo.xcprivacy']} |
|||
end |
|||
@ -0,0 +1,55 @@ |
|||
import 'package:flutter/services.dart'; |
|||
|
|||
class MusicService { |
|||
static final MusicService _instance = MusicService._internal(); |
|||
|
|||
/// 获取单例 |
|||
factory MusicService() => _instance; |
|||
|
|||
// 方法通道 |
|||
final MethodChannel _methodChannel = const MethodChannel( |
|||
'com.yunqiinnovation.music_service', |
|||
); |
|||
|
|||
// 事件通道 - 简化为只使用数据和状态通道 |
|||
late final EventChannel _dataEventChannel; |
|||
late final EventChannel _statusEventChannel; |
|||
|
|||
// 事件流 |
|||
late final Stream<dynamic> _dataStream; |
|||
late final Stream<dynamic> _statusStream; |
|||
|
|||
// 获取事件流 |
|||
Stream<dynamic> get onDataReceived => _dataStream; |
|||
|
|||
// 状态流直接传递原始数据 |
|||
Stream<dynamic> get onStatusUpdated => _statusStream; |
|||
|
|||
// 内部构造函数 |
|||
MusicService._internal() { |
|||
_dataEventChannel = const EventChannel( |
|||
'com.yunqiinnovation.music_service/data_events', |
|||
); |
|||
_statusEventChannel = const EventChannel( |
|||
'com.yunqiinnovation.music_service/status_events', |
|||
); |
|||
|
|||
// 初始化事件流 |
|||
_dataStream = _dataEventChannel.receiveBroadcastStream(); |
|||
_statusStream = _statusEventChannel.receiveBroadcastStream(); |
|||
} |
|||
|
|||
Future<void> playMusic(String url) async { |
|||
await _methodChannel.invokeMethod('play', {'url': url}); |
|||
} |
|||
|
|||
Future<void> pauseMusic() async => _methodChannel.invokeMethod('pause'); |
|||
Future<void> resumeMusic() async => _methodChannel.invokeMethod('resume'); |
|||
Future<void> nextTrack() async => _methodChannel.invokeMethod('next'); |
|||
Future<void> previousTrack() async => _methodChannel.invokeMethod('previous'); |
|||
Future<void> stopMusic() async => _methodChannel.invokeMethod('stop'); |
|||
Future<void> setVolume(double volume) async => _methodChannel.invokeMethod( |
|||
'setVolume', |
|||
{'volume': volume}, |
|||
); // volume: 0.0~1.0 |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
name: music_service |
|||
description: "A new Flutter project." |
|||
version: 0.0.1 |
|||
homepage: |
|||
|
|||
environment: |
|||
sdk: ^3.7.2 |
|||
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.music_service |
|||
pluginClass: MusicServicePlugin |
|||
ios: |
|||
pluginClass: MusicServicePlugin |
|||
|
|||
# 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 |
|||
@ -0,0 +1,249 @@ |
|||
import Foundation |
|||
|
|||
/// 工具类型枚举 |
|||
enum ToolType { |
|||
case localFunction // 本地函数 |
|||
case mcpTool // MCP工具 |
|||
} |
|||
|
|||
/// 函数处理器协议 |
|||
protocol FunctionHandler { |
|||
/// 处理函数调用 |
|||
/// - Parameters: |
|||
/// - arguments: 函数参数,Dictionary格式 |
|||
/// - Returns: 函数执行结果,字符串 |
|||
func handle(arguments: [String: Any]) async throws -> String |
|||
} |
|||
|
|||
/// MCPClient - iOS版本,负责连接到Model Context Protocol服务器和管理本地函数 |
|||
public class MCPClient { |
|||
private let TAG = "MCPClient" |
|||
|
|||
// 本地函数Map,函数名 -> 处理器 |
|||
private var localFunctions = [String: FunctionHandler]() |
|||
|
|||
// 工具定义Map |
|||
private var toolDefinitions = [[String: Any]]() |
|||
|
|||
// 是否已连接 |
|||
private var isConnected = false |
|||
|
|||
// 系统功能处理器 |
|||
private var systemFunctionHandler: SystemFunctionHandler? |
|||
|
|||
// SSE会话 |
|||
private var sseSession: URLSession? |
|||
private var sseTask: URLSessionDataTask? |
|||
|
|||
/// 初始化MCPClient |
|||
/// - Parameter context: 可选的应用上下文 |
|||
init(_ context: Any? = nil) { |
|||
if let appContext = context { |
|||
systemFunctionHandler = SystemFunctionHandler(context: appContext) |
|||
|
|||
// 初始化语音功能 |
|||
initializeVoiceFunctions() |
|||
} |
|||
} |
|||
|
|||
/// 初始化语音功能处理器 |
|||
private func initializeVoiceFunctions() { |
|||
do { |
|||
// 使用SystemFunctionHandler注册所有功能 |
|||
systemFunctionHandler?.registerAllFunctions(client: self) |
|||
|
|||
print("\(TAG) 语音功能初始化完成") |
|||
} catch { |
|||
print("\(TAG) 语音功能初始化失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
|
|||
/// 注册本地函数 |
|||
/// - Parameters: |
|||
/// - name: 函数名称 |
|||
/// - description: 函数描述 |
|||
/// - parameters: 函数参数定义 |
|||
/// - handler: 函数处理器 |
|||
/// - Returns: 是否注册成功 |
|||
func registerLocalFunction(name: String, description: String, parameters: [String: Any], handler: FunctionHandler) -> Bool { |
|||
do { |
|||
// 存储函数处理器 |
|||
localFunctions[name] = handler |
|||
|
|||
// 创建函数定义 |
|||
let functionDef: [String: Any] = [ |
|||
"name": name, |
|||
"description": description, |
|||
"parameters": parameters |
|||
] |
|||
|
|||
// 添加到工具定义列表 |
|||
let toolDef: [String: Any] = [ |
|||
"type": "function", |
|||
"function": functionDef |
|||
] |
|||
|
|||
// 更新或添加工具定义 |
|||
if let index = toolDefinitions.firstIndex(where: { |
|||
guard let function = $0["function"] as? [String: Any], |
|||
let functionName = function["name"] as? String else { |
|||
return false |
|||
} |
|||
return functionName == name |
|||
}) { |
|||
toolDefinitions[index] = toolDef |
|||
} else { |
|||
toolDefinitions.append(toolDef) |
|||
} |
|||
|
|||
print("\(TAG) 成功注册本地函数: \(name)") |
|||
return true |
|||
} catch { |
|||
print("\(TAG) 注册函数失败: \(error.localizedDescription)") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/// 连接到MCP服务器(通过SSE) |
|||
/// - Parameter mcpServer: 服务器URL |
|||
/// - Returns: 是否连接成功 |
|||
func connectToSSE(_ mcpServer: String) -> Bool { |
|||
// 关闭现有连接 |
|||
closeConnection() |
|||
|
|||
// 如果URL为空,返回失败 |
|||
if mcpServer.isEmpty { |
|||
print("\(TAG) MCP服务器URL为空") |
|||
return false |
|||
} |
|||
|
|||
// 解析服务器URL |
|||
guard let serverURL = URL(string: mcpServer) else { |
|||
print("\(TAG) 无效的MCP服务器URL: \(mcpServer)") |
|||
return false |
|||
} |
|||
|
|||
print("\(TAG) 正在连接到MCP服务器: \(mcpServer)") |
|||
|
|||
// 创建会话配置 |
|||
let config = URLSessionConfiguration.default |
|||
config.timeoutIntervalForRequest = 30 |
|||
config.timeoutIntervalForResource = 60 |
|||
|
|||
// 创建会话 |
|||
let session = URLSession(configuration: config) |
|||
self.sseSession = session |
|||
|
|||
// 创建请求 |
|||
var request = URLRequest(url: serverURL) |
|||
request.httpMethod = "GET" |
|||
request.addValue("text/event-stream", forHTTPHeaderField: "Accept") |
|||
|
|||
// 创建并启动SSE任务 |
|||
let task = session.dataTask(with: request) { [weak self] data, response, error in |
|||
guard let self = self else { return } |
|||
|
|||
if let error = error { |
|||
print("\(self.TAG) MCP服务器连接失败: \(error.localizedDescription)") |
|||
self.isConnected = false |
|||
return |
|||
} |
|||
|
|||
if let httpResponse = response as? HTTPURLResponse { |
|||
if httpResponse.statusCode == 200 { |
|||
print("\(self.TAG) 成功连接到MCP服务器") |
|||
self.isConnected = true |
|||
|
|||
// 处理服务器响应数据 |
|||
if let data = data, let responseString = String(data: data, encoding: .utf8) { |
|||
print("\(self.TAG) 收到初始响应: \(responseString)") |
|||
|
|||
// 在这里解析可用工具列表,暂时模拟 |
|||
// 实际情况下,需要实现SSE流解析 |
|||
} |
|||
} else { |
|||
print("\(self.TAG) MCP服务器连接失败,状态码: \(httpResponse.statusCode)") |
|||
self.isConnected = false |
|||
} |
|||
} |
|||
} |
|||
|
|||
task.resume() |
|||
self.sseTask = task |
|||
|
|||
// 在iOS中,连接是异步的,此处返回true表示连接已开始 |
|||
// 实际连接状态通过isConnected属性检查 |
|||
return true |
|||
} |
|||
|
|||
/// 检查是否已连接 |
|||
/// - Returns: 是否已连接 |
|||
func isConnected() -> Bool { |
|||
return isConnected |
|||
} |
|||
|
|||
/// 关闭连接 |
|||
func closeConnection() { |
|||
sseTask?.cancel() |
|||
sseTask = nil |
|||
isConnected = false |
|||
} |
|||
|
|||
/// 获取工具定义列表,用于OpenAI请求 |
|||
/// - Returns: 工具定义列表 |
|||
func getToolMaps() -> [[String: Any]] { |
|||
return toolDefinitions |
|||
} |
|||
|
|||
/// 解析JSON参数 |
|||
/// - Parameter argumentsJson: JSON字符串 |
|||
/// - Returns: 解析后的参数字典 |
|||
func parseJsonArguments(_ argumentsJson: String) -> [String: Any] { |
|||
guard !argumentsJson.isEmpty, |
|||
let data = argumentsJson.data(using: .utf8), |
|||
let parsedArgs = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { |
|||
return [:] |
|||
} |
|||
return parsedArgs |
|||
} |
|||
|
|||
/// 调用工具 |
|||
/// - Parameters: |
|||
/// - name: 工具名称 |
|||
/// - arguments: 工具参数 |
|||
/// - Returns: 工具调用结果 |
|||
func callTool(name: String, arguments: [String: Any]) async -> [String: Any] { |
|||
do { |
|||
// 检查是否为本地函数 |
|||
if let handler = localFunctions[name] { |
|||
let result = try await handler.handle(arguments: arguments) |
|||
return [ |
|||
"name": name, |
|||
"context": result |
|||
] |
|||
} |
|||
|
|||
// 如果不是本地函数,尝试作为MCP工具调用 |
|||
if isConnected { |
|||
// 这里应该实现真正的MCP工具调用 |
|||
// 暂时返回一个模拟结果 |
|||
return [ |
|||
"name": name, |
|||
"context": "模拟MCP工具调用结果: \(name)(\(arguments))" |
|||
] |
|||
} |
|||
|
|||
return ["context": "无法处理工具调用:找不到工具 \(name) 或MCP未连接"] |
|||
} catch { |
|||
print("\(TAG) 调用工具失败: \(error.localizedDescription)") |
|||
return ["context": "工具调用失败: \(error.localizedDescription)"] |
|||
} |
|||
} |
|||
|
|||
/// 关闭MCP客户端 |
|||
func close() { |
|||
closeConnection() |
|||
localFunctions.removeAll() |
|||
toolDefinitions.removeAll() |
|||
} |
|||
} |
|||
@ -0,0 +1,514 @@ |
|||
import Foundation |
|||
import UIKit |
|||
import CoreLocation |
|||
import Contacts |
|||
import EventKit |
|||
|
|||
/// 系统功能处理器 |
|||
class SystemFunctionHandler { |
|||
private let TAG = "SystemFunctionHandler" |
|||
private let context: Any |
|||
static let ACTION_EXIT_INTERACTION = "com.yunqiinnovation.deepsound.action.EXIT_INTERACTION" |
|||
|
|||
/// 初始化系统功能处理器 |
|||
/// - Parameter context: 应用上下文 |
|||
init(context: Any) { |
|||
self.context = context |
|||
} |
|||
|
|||
/// 注册所有系统功能 |
|||
/// - Parameter client: MCP客户端 |
|||
func registerAllFunctions(client: MCPClient) { |
|||
// 注册退出交互函数 |
|||
registerExitInteractionFunction(client) |
|||
|
|||
// 注册翻译模式函数 |
|||
registerTranslationModeFunction(client) |
|||
|
|||
// 注册发送短信函数 |
|||
registerSendTextMessageFunction(client) |
|||
|
|||
// 注册拨打电话函数 |
|||
registerMakePhoneCallFunction(client) |
|||
|
|||
// 注册打开应用函数 |
|||
registerOpenAppFunction(client) |
|||
|
|||
// 注册设置提醒函数 |
|||
registerSetReminderFunction(client) |
|||
|
|||
// 注册获取当前时间函数 |
|||
registerGetCurrentTimeFunction(client) |
|||
|
|||
// 注册获取当前位置函数 |
|||
registerGetCurrentLocationFunction(client) |
|||
|
|||
print("\(TAG) 所有系统功能注册完成") |
|||
} |
|||
|
|||
/// 注册退出交互函数 |
|||
private func registerExitInteractionFunction(_ client: MCPClient) { |
|||
// 定义函数参数 |
|||
let parameters: [String: Any] = [ |
|||
"type": "object", |
|||
"properties": [:], |
|||
"required": [] |
|||
] |
|||
|
|||
// 创建函数处理器 |
|||
let handler = ExitInteractionHandler() |
|||
|
|||
// 注册函数 |
|||
client.registerLocalFunction( |
|||
name: "exit_interaction", |
|||
description: "结束当前交互", |
|||
parameters: parameters, |
|||
handler: handler |
|||
) |
|||
print("\(TAG) 已注册退出交互功能") |
|||
} |
|||
|
|||
/// 注册翻译模式函数 |
|||
private func registerTranslationModeFunction(_ client: MCPClient) { |
|||
// 定义函数参数 |
|||
let parameters: [String: Any] = [ |
|||
"type": "object", |
|||
"properties": [:], |
|||
"required": [] |
|||
] |
|||
|
|||
// 创建函数处理器 |
|||
let handler = TranslationModeHandler() |
|||
|
|||
// 注册函数 |
|||
client.registerLocalFunction( |
|||
name: "enter_translation_mode", |
|||
description: "用户请求进入实时翻译模式时,启动实时翻译功能", |
|||
parameters: parameters, |
|||
handler: handler |
|||
) |
|||
print("\(TAG) 已注册翻译模式功能") |
|||
} |
|||
|
|||
/// 注册发送短信函数 |
|||
private func registerSendTextMessageFunction(_ client: MCPClient) { |
|||
// 定义函数参数 |
|||
let parameters: [String: Any] = [ |
|||
"type": "object", |
|||
"properties": [ |
|||
"contact": [ |
|||
"type": "string", |
|||
"description": "联系人姓名或电话号码" |
|||
], |
|||
"message": [ |
|||
"type": "string", |
|||
"description": "短信内容" |
|||
] |
|||
], |
|||
"required": ["contact", "message"] |
|||
] |
|||
|
|||
// 创建函数处理器 |
|||
let handler = SendTextMessageHandler() |
|||
|
|||
// 注册函数 |
|||
client.registerLocalFunction( |
|||
name: "send_text_message", |
|||
description: "发送短信", |
|||
parameters: parameters, |
|||
handler: handler |
|||
) |
|||
print("\(TAG) 已注册发送短信功能") |
|||
} |
|||
|
|||
/// 注册拨打电话函数 |
|||
private func registerMakePhoneCallFunction(_ client: MCPClient) { |
|||
// 定义函数参数 |
|||
let parameters: [String: Any] = [ |
|||
"type": "object", |
|||
"properties": [ |
|||
"contact": [ |
|||
"type": "string", |
|||
"description": "联系人姓名或电话号码" |
|||
] |
|||
], |
|||
"required": ["contact"] |
|||
] |
|||
|
|||
// 创建函数处理器 |
|||
let handler = MakePhoneCallHandler() |
|||
|
|||
// 注册函数 |
|||
client.registerLocalFunction( |
|||
name: "make_phone_call", |
|||
description: "拨打电话", |
|||
parameters: parameters, |
|||
handler: handler |
|||
) |
|||
print("\(TAG) 已注册拨打电话功能") |
|||
} |
|||
|
|||
/// 注册打开应用函数 |
|||
private func registerOpenAppFunction(_ client: MCPClient) { |
|||
// 定义函数参数 |
|||
let parameters: [String: Any] = [ |
|||
"type": "object", |
|||
"properties": [ |
|||
"app_name": [ |
|||
"type": "string", |
|||
"description": "应用名称" |
|||
] |
|||
], |
|||
"required": ["app_name"] |
|||
] |
|||
|
|||
// 创建函数处理器 |
|||
let handler = OpenAppHandler() |
|||
|
|||
// 注册函数 |
|||
client.registerLocalFunction( |
|||
name: "open_app", |
|||
description: "当用户请求打开特定应用时,启动相应应用", |
|||
parameters: parameters, |
|||
handler: handler |
|||
) |
|||
print("\(TAG) 已注册打开应用功能") |
|||
} |
|||
|
|||
/// 注册设置提醒函数 |
|||
private func registerSetReminderFunction(_ client: MCPClient) { |
|||
// 定义函数参数 |
|||
let parameters: [String: Any] = [ |
|||
"type": "object", |
|||
"properties": [ |
|||
"title": [ |
|||
"type": "string", |
|||
"description": "提醒标题" |
|||
], |
|||
"content": [ |
|||
"type": "string", |
|||
"description": "提醒内容" |
|||
], |
|||
"time": [ |
|||
"type": "string", |
|||
"description": "提醒时间,格式为'yyyy-MM-dd HH:mm',如'2023-12-31 14:30'" |
|||
] |
|||
], |
|||
"required": ["title", "time"] |
|||
] |
|||
|
|||
// 创建函数处理器 |
|||
let handler = SetReminderHandler() |
|||
|
|||
// 注册函数 |
|||
client.registerLocalFunction( |
|||
name: "set_reminder", |
|||
description: "设置提醒事项", |
|||
parameters: parameters, |
|||
handler: handler |
|||
) |
|||
print("\(TAG) 已注册设置提醒功能") |
|||
} |
|||
|
|||
/// 注册获取当前时间函数 |
|||
private func registerGetCurrentTimeFunction(_ client: MCPClient) { |
|||
// 定义函数参数 |
|||
let parameters: [String: Any] = [ |
|||
"type": "object", |
|||
"properties": [ |
|||
"format": [ |
|||
"type": "string", |
|||
"description": "时间格式,可选,默认为标准格式" |
|||
] |
|||
], |
|||
"required": [] |
|||
] |
|||
|
|||
// 创建函数处理器 |
|||
let handler = GetCurrentTimeHandler() |
|||
|
|||
// 注册函数 |
|||
client.registerLocalFunction( |
|||
name: "get_current_time", |
|||
description: "获取当前日期和时间", |
|||
parameters: parameters, |
|||
handler: handler |
|||
) |
|||
print("\(TAG) 已注册获取当前时间功能") |
|||
} |
|||
|
|||
/// 注册获取当前位置函数 |
|||
private func registerGetCurrentLocationFunction(_ client: MCPClient) { |
|||
// 定义函数参数 |
|||
let parameters: [String: Any] = [ |
|||
"type": "object", |
|||
"properties": [:], |
|||
"required": [] |
|||
] |
|||
|
|||
// 创建函数处理器 |
|||
let handler = GetCurrentLocationHandler() |
|||
|
|||
// 注册函数 |
|||
client.registerLocalFunction( |
|||
name: "get_current_location", |
|||
description: "获取当前地理位置", |
|||
parameters: parameters, |
|||
handler: handler |
|||
) |
|||
print("\(TAG) 已注册获取当前位置功能") |
|||
} |
|||
} |
|||
|
|||
/// 退出交互处理器 |
|||
class ExitInteractionHandler: FunctionHandler { |
|||
func handle(arguments: [String: Any]) async throws -> String { |
|||
// 发送通知和广播 |
|||
NotificationCenter.default.post( |
|||
name: Notification.Name(SystemFunctionHandler.ACTION_EXIT_INTERACTION), |
|||
object: nil, |
|||
userInfo: ["timestamp": Date().timeIntervalSince1970] |
|||
) |
|||
|
|||
return "{\"result\": \"已结束当前交互\"}" |
|||
} |
|||
} |
|||
|
|||
/// 翻译模式处理器 |
|||
class TranslationModeHandler: FunctionHandler { |
|||
func handle(arguments: [String: Any]) async throws -> String { |
|||
// 发送通知 |
|||
NotificationCenter.default.post( |
|||
name: Notification.Name("com.yunqiinnovation.deepsound.ACTION_ENTER_TRANSLATION_MODE"), |
|||
object: nil, |
|||
userInfo: ["timestamp": Date().timeIntervalSince1970] |
|||
) |
|||
|
|||
return "{\"result\": \"已进入实时翻译模式\"}" |
|||
} |
|||
} |
|||
|
|||
/// 发送短信处理器 |
|||
class SendTextMessageHandler: FunctionHandler { |
|||
func handle(arguments: [String: Any]) async throws -> String { |
|||
guard let contact = arguments["contact"] as? String, |
|||
let message = arguments["message"] as? String else { |
|||
return "{\"result\": \"发送短信失败:缺少联系人或消息内容\"}" |
|||
} |
|||
|
|||
// iOS不允许应用直接发送短信,但可以打开短信应用 |
|||
if let url = URL(string: "sms:\(contact)&body=\(message.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")") { |
|||
DispatchQueue.main.async { |
|||
UIApplication.shared.open(url, options: [:], completionHandler: nil) |
|||
} |
|||
return "{\"result\": \"已打开短信应用,收件人:\(contact)\"}" |
|||
} |
|||
|
|||
return "{\"result\": \"发送短信失败:无法打开短信应用\"}" |
|||
} |
|||
} |
|||
|
|||
/// 拨打电话处理器 |
|||
class MakePhoneCallHandler: FunctionHandler { |
|||
func handle(arguments: [String: Any]) async throws -> String { |
|||
guard let contact = arguments["contact"] as? String else { |
|||
return "{\"result\": \"拨打电话失败:缺少联系人\"}" |
|||
} |
|||
|
|||
let phoneNumber = contact.replacingOccurrences(of: "[^+0-9]", with: "", options: .regularExpression) |
|||
|
|||
if let url = URL(string: "tel://\(phoneNumber)") { |
|||
DispatchQueue.main.async { |
|||
UIApplication.shared.open(url, options: [:], completionHandler: nil) |
|||
} |
|||
return "{\"result\": \"正在拨打\(contact)的电话\"}" |
|||
} |
|||
|
|||
return "{\"result\": \"拨打电话失败:无法拨打电话\"}" |
|||
} |
|||
} |
|||
|
|||
/// 打开应用处理器 |
|||
class OpenAppHandler: FunctionHandler { |
|||
func handle(arguments: [String: Any]) async throws -> String { |
|||
guard let appName = arguments["app_name"] as? String else { |
|||
return "{\"result\": \"打开应用失败:缺少应用名称\"}" |
|||
} |
|||
|
|||
// iOS没有直接打开其他应用的API,只能通过URL Scheme |
|||
// 这里只是模拟返回成功,实际上无法在iOS上实现这个功能 |
|||
return "{\"result\": \"打开应用功能在iOS上受限,无法直接打开\(appName)\"}" |
|||
} |
|||
} |
|||
|
|||
/// 设置提醒处理器 |
|||
class SetReminderHandler: FunctionHandler { |
|||
func handle(arguments: [String: Any]) async throws -> String { |
|||
guard let title = arguments["title"] as? String, |
|||
let timeStr = arguments["time"] as? String else { |
|||
return "{\"result\": \"设置提醒失败:缺少标题或时间\"}" |
|||
} |
|||
|
|||
let content = arguments["content"] as? String ?? "" |
|||
|
|||
// 解析时间 |
|||
let dateFormatter = DateFormatter() |
|||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm" |
|||
guard let date = dateFormatter.date(from: timeStr) else { |
|||
return "{\"result\": \"设置提醒失败:时间格式无效,请使用'yyyy-MM-dd HH:mm'格式\"}" |
|||
} |
|||
|
|||
// 使用EventKit添加提醒 |
|||
let eventStore = EKEventStore() |
|||
let event = EKEvent(eventStore: eventStore) |
|||
event.title = title |
|||
event.notes = content |
|||
event.startDate = date |
|||
event.endDate = date.addingTimeInterval(3600) // 1小时后 |
|||
event.calendar = eventStore.defaultCalendarForNewEvents |
|||
|
|||
do { |
|||
try eventStore.save(event, span: .thisEvent) |
|||
return "{\"result\": \"已创建提醒: \(title), 时间: \(timeStr)\"}" |
|||
} catch { |
|||
return "{\"result\": \"设置提醒失败:\(error.localizedDescription)\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 获取当前时间处理器 |
|||
class GetCurrentTimeHandler: FunctionHandler { |
|||
func handle(arguments: [String: Any]) async throws -> String { |
|||
let format = arguments["format"] as? String |
|||
|
|||
let calendar = Calendar.current |
|||
let date = Date() |
|||
|
|||
let dateFormatter = DateFormatter() |
|||
dateFormatter.locale = Locale(identifier: "zh_CN") |
|||
|
|||
let timeStr: String |
|||
switch format?.lowercased() { |
|||
case "short": |
|||
dateFormatter.dateFormat = "HH:mm" |
|||
timeStr = dateFormatter.string(from: date) |
|||
case "date": |
|||
dateFormatter.dateFormat = "yyyy年MM月dd日" |
|||
timeStr = dateFormatter.string(from: date) |
|||
case "time": |
|||
dateFormatter.dateFormat = "HH:mm:ss" |
|||
timeStr = dateFormatter.string(from: date) |
|||
case "full": |
|||
dateFormatter.dateFormat = "yyyy年MM月dd日 EEEE HH:mm:ss" |
|||
timeStr = dateFormatter.string(from: date) |
|||
case "iso": |
|||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" |
|||
timeStr = dateFormatter.string(from: date) |
|||
default: |
|||
dateFormatter.dateFormat = "yyyy年MM月dd日 HH:mm" |
|||
timeStr = dateFormatter.string(from: date) |
|||
} |
|||
|
|||
// 获取星期几 |
|||
let weekDay: String |
|||
if format == "full" { |
|||
weekDay = "" // 已经包含在dateFormatter里 |
|||
} else { |
|||
let weekDayNum = calendar.component(.weekday, from: date) |
|||
switch weekDayNum { |
|||
case 1: weekDay = "星期日" |
|||
case 2: weekDay = "星期一" |
|||
case 3: weekDay = "星期二" |
|||
case 4: weekDay = "星期三" |
|||
case 5: weekDay = "星期四" |
|||
case 6: weekDay = "星期五" |
|||
case 7: weekDay = "星期六" |
|||
default: weekDay = "" |
|||
} |
|||
} |
|||
|
|||
// 格式化结果 |
|||
let resultTime: String |
|||
if format == "short" || format == "time" { |
|||
resultTime = timeStr |
|||
} else if format == "full" { |
|||
resultTime = timeStr // already contains weekday |
|||
} else { |
|||
resultTime = "\(timeStr) \(weekDay)" |
|||
} |
|||
|
|||
return "{\"result\": \"\(resultTime)\", \"time\": \"\(resultTime)\"}" |
|||
} |
|||
} |
|||
|
|||
/// 获取当前位置处理器 |
|||
class GetCurrentLocationHandler: FunctionHandler { |
|||
private let locationManager = CLLocationManager() |
|||
private var locationCompletionHandler: ((String, Bool) -> Void)? |
|||
|
|||
func handle(arguments: [String: Any]) async throws -> String { |
|||
return await withCheckedContinuation { continuation in |
|||
self.getLocation { message, success in |
|||
continuation.resume(returning: "{\"result\": \"\(message)\", \"success\": \(success)}") |
|||
} |
|||
} |
|||
} |
|||
|
|||
private func getLocation(completion: @escaping (String, Bool) -> Void) { |
|||
locationCompletionHandler = completion |
|||
|
|||
// 检查权限 |
|||
let authStatus = CLLocationManager.authorizationStatus() |
|||
if authStatus == .denied || authStatus == .restricted { |
|||
completion("需要位置权限才能获取当前位置", false) |
|||
return |
|||
} |
|||
|
|||
// 检查位置服务是否开启 |
|||
if !CLLocationManager.locationServicesEnabled() { |
|||
completion("位置服务未启用,请打开GPS或网络定位", false) |
|||
return |
|||
} |
|||
|
|||
// 如果有最后的位置,使用它 |
|||
if let lastLocation = locationManager.location { |
|||
let message = "当前位置: 经度 \(lastLocation.coordinate.longitude), 纬度 \(lastLocation.coordinate.latitude)" |
|||
completion(message, true) |
|||
return |
|||
} |
|||
|
|||
// 否则请求更新 |
|||
locationManager.delegate = self |
|||
locationManager.desiredAccuracy = kCLLocationAccuracyBest |
|||
locationManager.requestWhenInUseAuthorization() |
|||
locationManager.startUpdatingLocation() |
|||
|
|||
// 设置超时 |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in |
|||
if let handler = self?.locationCompletionHandler { |
|||
self?.locationManager.stopUpdatingLocation() |
|||
handler("获取位置超时,请稍后重试", false) |
|||
self?.locationCompletionHandler = nil |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 扩展以接收位置更新 |
|||
extension GetCurrentLocationHandler: CLLocationManagerDelegate { |
|||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { |
|||
guard let location = locations.last, let handler = locationCompletionHandler else { return } |
|||
|
|||
manager.stopUpdatingLocation() |
|||
|
|||
let message = "当前位置: 经度 \(location.coordinate.longitude), 纬度 \(location.coordinate.latitude)" |
|||
handler(message, true) |
|||
locationCompletionHandler = nil |
|||
} |
|||
|
|||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { |
|||
manager.stopUpdatingLocation() |
|||
locationCompletionHandler?("获取位置失败: \(error.localizedDescription)", false) |
|||
locationCompletionHandler = nil |
|||
} |
|||
} |
|||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue