diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 9f7690714..6879ee469 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -65,6 +65,9 @@ dependencies { // 添加legacy-support-v4依赖 implementation("androidx.legacy:legacy-support-v4:1.0.0") + // 添加 AndroidX Security 加密 SharedPreferences 依赖 + implementation("androidx.security:security-crypto:1.1.0-alpha06") + // // 添加火山语音合成SDK依赖 // implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8") diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 97a32b9e3..88f1fef92 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -17,8 +17,10 @@ - - + + diff --git a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt index 1eff8c8be..9ed28070d 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt @@ -1,20 +1,15 @@ package com.example.deep_voice -import android.util.Log +import android.content.Context +import com.example.deep_voice.core.utils.FileLogger import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.util.EventHandler import java.util.concurrent.ExecutionException -import android.media.audiofx.AcousticEchoCanceler -import android.media.AudioRecord -import android.media.MediaRecorder -import android.media.AudioFormat -import java.util.concurrent.Executors -class AzureAsrHelper { +class AzureAsrHelper(private val context: Context) { private var recognizer: SpeechRecognizer? = null private var speechConfig: SpeechConfig? = null - private var audioConfig: AudioConfig? = null private val TAG = "AzureAsrHelper" private var isContinuousRecognitionActive = false private var currentLanguage = "zh-CN" @@ -22,34 +17,26 @@ class AzureAsrHelper { private var serviceRegion = "" private var isAutoDetectLanguage = false private var supportedLanguages = arrayOf("zh-CN", "en-US") - - // 回音消除相关 - private var echoCanceler: AcousticEchoCanceler? = null - private var audioSessionId: Int = -1 - private var isEchoCancellationEnabled = false - - // 直接管理 AudioRecord 和 PushStream - private var audioRecord: AudioRecord? = null - private var pushStream: PushAudioInputStream? = null - private var isRecording = false - private val executorService = Executors.newSingleThreadExecutor() - // 初始化 SDK + // 初始化SDK并创建recognizer fun initialize(subscriptionKey: String, serviceRegion: String, supportedLanguages: Array = arrayOf("zh-CN", "en-US")): Boolean { try { - Log.d(TAG, "开始初始化 Azure 语音服务: region=$serviceRegion, supportedLanguages=${supportedLanguages.joinToString()}") + FileLogger.d(TAG, "初始化 Azure 语音服务") // 检查配置是否为空 if (subscriptionKey.isEmpty() || serviceRegion.isEmpty()) { - Log.e(TAG, "Azure 配置信息不完整: subscriptionKey=${subscriptionKey.isEmpty()}, serviceRegion=${serviceRegion.isEmpty()}") + FileLogger.e(TAG, "Azure 配置信息不完整") return false } + // 释放之前的资源 + dispose() + this.subscriptionKey = subscriptionKey this.serviceRegion = serviceRegion - // 设置支持的语言列表 + // 设置语言 if (supportedLanguages.isNotEmpty()) { this.supportedLanguages = supportedLanguages } @@ -62,320 +49,178 @@ class AzureAsrHelper { this.currentLanguage = supportedLanguages[0] } - // 释放之前的资源 - dispose() - // 创建语音配置 speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) - // 设置为 CONVERSATION 模式 - speechConfig?.setProperty(PropertyId.SpeechServiceConnection_RecoMode, "CONVERSATION") - + // 设置语言配置 if (isAutoDetectLanguage) { - // 设置自动语言检测模式为连续检测 + // 设置自动语言检测 speechConfig?.setProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous") } else { // 设置指定的识别语言 speechConfig?.speechRecognitionLanguage = currentLanguage } - // 创建 PushAudioInputStream 和 AudioConfig + // 创建识别器 try { - setupAudioInput() - Log.d(TAG, "成功创建音频配置") + if (isAutoDetectLanguage) { + val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) + recognizer = SpeechRecognizer(speechConfig, autoDetectConfig) + } else { + recognizer = SpeechRecognizer(speechConfig) + } + + FileLogger.d(TAG, "Azure 语音服务初始化成功") + return true } catch (e: Exception) { - Log.e(TAG, "创建音频配置失败: ${e.message}", e) + FileLogger.e(TAG, "创建识别器失败: ${e.message}") return false } - - // 创建识别器 - Log.d(TAG, "创建语音识别器") - if (isAutoDetectLanguage) { - // 创建自动语言检测配置 - val autoDetectSourceLanguageConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) - recognizer = SpeechRecognizer(speechConfig, autoDetectSourceLanguageConfig, audioConfig) - } else { - recognizer = SpeechRecognizer(speechConfig, audioConfig) - } - - Log.d(TAG, "Azure 语音服务初始化成功") - return true } catch (e: Exception) { - Log.e(TAG, "初始化失败: ${e.message}", e) + FileLogger.e(TAG, "初始化失败: ${e.message}") return false } } - // 设置音频输入 - private fun setupAudioInput() { - // 释放之前的资源 - releaseAudioResources() - - // 创建 PushAudioInputStream - pushStream = AudioInputStream.createPushStream() - - // 创建 AudioConfig - audioConfig = AudioConfig.fromStreamInput(pushStream) - - // 创建 AudioRecord - val sampleRate = 16000 - val channelConfig = AudioFormat.CHANNEL_IN_MONO - val audioFormat = AudioFormat.ENCODING_PCM_16BIT - val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) - - if (bufferSize <= 0) { - Log.e(TAG, "无法获取最小缓冲区大小") - throw RuntimeException("无法获取最小缓冲区大小") - } - - audioRecord = AudioRecord( - MediaRecorder.AudioSource.VOICE_COMMUNICATION, - sampleRate, - channelConfig, - audioFormat, - bufferSize - ) - - if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { - Log.e(TAG, "AudioRecord 初始化失败") - audioRecord?.release() - audioRecord = null - throw RuntimeException("AudioRecord 初始化失败") + // 重置 recognizer + private fun resetRecognizer(): Boolean { + try { + // 释放之前的 recognizer + recognizer?.close() + recognizer = null + + // 使用现有配置重新创建 recognizer + if (speechConfig != null) { + if (isAutoDetectLanguage) { + val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) + recognizer = SpeechRecognizer(speechConfig, autoDetectConfig) + } else { + recognizer = SpeechRecognizer(speechConfig) + } + return true + } else { + FileLogger.e(TAG, "语音配置未初始化") + return false + } + } catch (e: Exception) { + FileLogger.e(TAG, "重置识别器失败: ${e.message}") + return false } - - // 获取音频会话 ID 并设置回音消除 - audioSessionId = audioRecord?.audioSessionId ?: -1 - setupEchoCancellation() } // 开始一次性语音识别 - fun recognizeOnce(language: String? = null, callback: RecognizeCallback) { - if (recognizer == null) { - callback.onError("SpeechRecognizer 未初始化") + fun recognizeOnce(callback: RecognizeCallback) { + if (speechConfig == null) { + callback.onError("语音服务未初始化") return } - // 如果指定了新的语言,且不是自动检测模式,需要重新初始化 - if (language != null && language != currentLanguage && !isAutoDetectLanguage) { - // 使用指定的语言创建新的语言列表 - val newLanguages = arrayOf(language) - initialize(subscriptionKey, serviceRegion, newLanguages) + // 重置 recognizer + if (!resetRecognizer()) { + callback.onError("重置识别器失败") + return } try { - // 开始录音并推送数据 - startRecording() - + // 执行识别 val result = recognizer?.recognizeOnceAsync()?.get() - // 停止录音 - stopRecording() - if (result != null && result.reason == ResultReason.RecognizedSpeech) { - val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(result) - val detectedLanguage = autoDetectSourceLanguageResult?.language ?: "" - Log.d(TAG, "识别结果: ${result.text}, 检测到的语言: $detectedLanguage") - callback.onResult(result.text, detectedLanguage) + val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language + + callback.onResult(result.text, detectedLanguage ?: "") } else { callback.onError("未能识别语音") } } catch (e: Exception) { - stopRecording() callback.onError("识别异常: ${e.message}") } } // 开始连续语音识别 fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { - if (recognizer == null) { - Log.e(TAG, "SpeechRecognizer 未初始化") - callback.onError("SpeechRecognizer 未初始化") + if (speechConfig == null) { + callback.onError("语音服务未初始化") return false } // 如果已经在进行连续识别,先停止 if (isContinuousRecognitionActive) { - Log.d(TAG, "已经在进行连续识别,先停止") stopContinuousRecognition(callback) } - + + // 重置 recognizer + if (!resetRecognizer()) { + callback.onError("重置识别器失败") + return false + } + try { - Log.d(TAG, "开始重置识别器和音频资源") - - // 完全释放所有资源,确保干净的状态 - try { - // 释放识别器 - recognizer?.close() - recognizer = null - - // 释放音频资源 - releaseAudioResources() - - // 确保状态标志正确 - isContinuousRecognitionActive = false - isRecording = false - - Log.d(TAG, "资源释放完成,准备重新创建") - } catch (e: Exception) { - Log.e(TAG, "释放资源失败: ${e.message}", e) - // 继续执行,尝试重新创建 - } - - // 重新创建音频配置 - try { - setupAudioInput() - Log.d(TAG, "音频配置创建成功") - } catch (e: Exception) { - Log.e(TAG, "创建音频配置失败: ${e.message}", e) - callback.onError("创建音频配置失败: ${e.message}") - return false - } - - // 重新创建识别器 - try { - Log.d(TAG, "创建新的语音识别器") - if (isAutoDetectLanguage) { - // 创建自动语言检测配置 - val autoDetectSourceLanguageConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) - recognizer = SpeechRecognizer(speechConfig, autoDetectSourceLanguageConfig, audioConfig) - } else { - recognizer = SpeechRecognizer(speechConfig, audioConfig) - } - } catch (e: Exception) { - Log.e(TAG, "创建识别器失败: ${e.message}", e) - callback.onError("创建识别器失败: ${e.message}") - return false - } - // 设置识别事件处理 - recognizer?.let { recognizer -> - // 最终识别结果 - recognizer.recognized.addEventListener( - EventHandler { _, event -> - if (event.result.reason == ResultReason.RecognizedSpeech) { - val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(event.result) - val detectedLanguage = autoDetectSourceLanguageResult?.language ?: "" - Log.d(TAG, "识别结果: ${event.result.text}, 检测到的语言: $detectedLanguage") - callback.onResult(event.result.text, detectedLanguage) + // 最终识别结果 + recognizer?.recognized?.addEventListener( + EventHandler { _, event -> + if (event.result.reason == ResultReason.RecognizedSpeech) { + val detectedLanguage = if (isAutoDetectLanguage) { + AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" } else { - Log.d(TAG, "识别结果原因: ${event.result.reason}") + currentLanguage } + callback.onResult(event.result.text, detectedLanguage) } - ) + } + ) - // 识别中事件 - recognizer.recognizing.addEventListener( - EventHandler { _, event -> - if (event.result.reason == ResultReason.RecognizingSpeech) { - val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(event.result) - val detectedLanguage = autoDetectSourceLanguageResult?.language ?: "" - // if (isAutoDetectLanguage) { - // Log.d(TAG, "正在识别: ${event.result.text}, 检测到的语言: $detectedLanguage") - // } - callback.onRecognizing(event.result.text, detectedLanguage) + // 识别中事件 + recognizer?.recognizing?.addEventListener( + EventHandler { _, event -> + if (event.result.reason == ResultReason.RecognizingSpeech) { + val detectedLanguage = if (isAutoDetectLanguage) { + AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" + } else { + currentLanguage } + callback.onRecognizing(event.result.text, detectedLanguage) } - ) - - // 会话开始事件 - recognizer.sessionStarted.addEventListener( - EventHandler { _, _ -> - Log.d(TAG, "会话开始") - isContinuousRecognitionActive = true - callback.onSessionStarted() - } - ) + } + ) - // 会话结束事件 - recognizer.sessionStopped.addEventListener( - EventHandler { _, _ -> - Log.d(TAG, "会话结束") - isContinuousRecognitionActive = false - stopRecording() - callback.onSessionStopped() - } - ) + // 会话事件 + recognizer?.sessionStarted?.addEventListener( + EventHandler { _, _ -> + isContinuousRecognitionActive = true + callback.onSessionStarted() + } + ) - // 取消事件 - recognizer.canceled.addEventListener( - EventHandler { _, event -> - val errorDetails = if (event.reason == CancellationReason.Error) event.errorDetails else "" - Log.e(TAG, "识别取消: ${event.reason}, 详情: $errorDetails") - callback.onCanceled(event.reason.toString(), errorDetails) - isContinuousRecognitionActive = false - stopRecording() - } - ) + recognizer?.sessionStopped?.addEventListener( + EventHandler { _, _ -> + isContinuousRecognitionActive = false + callback.onSessionStopped() + } + ) - // 开始录音 - startRecording() - - // 开始连续识别 - Log.d(TAG, "开始连续识别") - recognizer.startContinuousRecognitionAsync().get() - Log.d(TAG, "连续识别已启动") - isContinuousRecognitionActive = true - } + // 取消事件 + recognizer?.canceled?.addEventListener( + EventHandler { _, event -> + val errorDetails = if (event.reason == CancellationReason.Error) event.errorDetails else "" + callback.onCanceled(event.reason.toString(), errorDetails) + isContinuousRecognitionActive = false + } + ) + + // 开始连续识别 + recognizer?.startContinuousRecognitionAsync()?.get() + isContinuousRecognitionActive = true return true } catch (e: Exception) { - Log.e(TAG, "开始连续识别失败: ${e.message}", e) callback.onError("开始连续识别失败: ${e.message}") isContinuousRecognitionActive = false - stopRecording() return false } } - // 开始录音并推送数据到 PushStream - private fun startRecording() { - if (isRecording || audioRecord == null || pushStream == null) { - return - } - - isRecording = true - audioRecord?.startRecording() - - executorService.submit { - val bufferSize = AudioRecord.getMinBufferSize( - 16000, - AudioFormat.CHANNEL_IN_MONO, - AudioFormat.ENCODING_PCM_16BIT - ) - val buffer = ByteArray(bufferSize) - - while (isRecording && audioRecord != null && pushStream != null) { - try { - val readBytes = audioRecord?.read(buffer, 0, buffer.size) ?: 0 - if (readBytes > 0) { - // 修复:PushAudioInputStream.write() 只接受一个 ByteArray 参数 - // 如果读取的数据长度等于缓冲区大小,直接写入 - if (readBytes == buffer.size) { - pushStream?.write(buffer) - } else { - // 否则,创建一个新的数组,只包含实际读取的数据 - val audioData = buffer.copyOf(readBytes) - pushStream?.write(audioData) - } - } - } catch (e: Exception) { - Log.e(TAG, "读取音频数据失败: ${e.message}", e) - break - } - } - } - } - - // 停止录音 - private fun stopRecording() { - isRecording = false - try { - audioRecord?.stop() - } catch (e: Exception) { - Log.e(TAG, "停止录音失败: ${e.message}", e) - } - } - // 停止连续语音识别 fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { if (!isContinuousRecognitionActive || recognizer == null) { @@ -385,13 +230,11 @@ class AzureAsrHelper { try { recognizer?.stopContinuousRecognitionAsync()?.get() isContinuousRecognitionActive = false - stopRecording() callback.onSessionStopped() return true } catch (e: Exception) { callback.onError("停止连续识别失败: ${e.message}") isContinuousRecognitionActive = false - stopRecording() return false } } @@ -400,98 +243,23 @@ class AzureAsrHelper { fun isContinuousRecognitionActive(): Boolean { return isContinuousRecognitionActive } - - // 获取当前语言 - fun getCurrentLanguage(): String { - return currentLanguage - } - // 释放资源 + // 释放所有资源 fun dispose() { try { - // 添加标志,避免重复释放 - if (recognizer == null && audioRecord == null && pushStream == null) { - Log.d(TAG, "资源已经释放,跳过重复释放") - return - } - - Log.d(TAG, "开始释放资源") - - if (isContinuousRecognitionActive && recognizer != null) { - try { - recognizer?.stopContinuousRecognitionAsync()?.get() - Log.d(TAG, "连续识别已停止") - } catch (e: Exception) { - Log.e(TAG, "停止连续识别失败: ${e.message}", e) - } - } - - try { - recognizer?.close() - recognizer = null - Log.d(TAG, "识别器已关闭") - } catch (e: Exception) { - Log.e(TAG, "关闭识别器失败: ${e.message}", e) - } + recognizer?.close() + recognizer = null - releaseAudioResources() - - try { - speechConfig?.close() - speechConfig = null - Log.d(TAG, "语音配置已关闭") - } catch (e: Exception) { - Log.e(TAG, "关闭语音配置失败: ${e.message}", e) - } + speechConfig?.close() + speechConfig = null isContinuousRecognitionActive = false - Log.d(TAG, "所有资源已释放") + + FileLogger.d(TAG, "语音识别资源已释放") } catch (e: Exception) { - Log.e(TAG, "释放资源时出错: ${e.message}", e) + FileLogger.e(TAG, "释放资源出错: ${e.message}") } } - - // 释放音频相关资源 - private fun releaseAudioResources() { - // 停止录音 - stopRecording() - - // 释放 AudioRecord - if (audioRecord != null) { - try { - Log.d(TAG, "释放 AudioRecord") - audioRecord?.release() - audioRecord = null - } catch (e: Exception) { - Log.e(TAG, "释放 AudioRecord 失败: ${e.message}", e) - } - } - - // 释放 AudioConfig - if (audioConfig != null) { - try { - Log.d(TAG, "释放 AudioConfig") - audioConfig?.close() - audioConfig = null - } catch (e: Exception) { - Log.e(TAG, "释放 AudioConfig 失败: ${e.message}", e) - } - } - - // 释放 PushStream - if (pushStream != null) { - try { - Log.d(TAG, "释放 PushStream") - pushStream?.close() - pushStream = null - } catch (e: Exception) { - Log.e(TAG, "释放 PushStream 失败: ${e.message}", e) - } - } - - // 释放回音消除资源 - releaseEchoCancellation() - } // 一次性识别回调接口 interface RecognizeCallback { @@ -509,127 +277,4 @@ class AzureAsrHelper { fun onError(error: String) } - // 设置回音消除 - private fun setupEchoCancellation() { - try { - // 检查设备是否支持回音消除 - if (AcousticEchoCanceler.isAvailable()) { - Log.d(TAG, "设备支持回音消除") - - // 使用实际的 AudioRecord 会话 ID - if (audioSessionId != -1) { - echoCanceler = AcousticEchoCanceler.create(audioSessionId) - if (echoCanceler != null) { - echoCanceler?.enabled = true - isEchoCancellationEnabled = echoCanceler?.enabled == true - Log.d(TAG, "回音消除已启用: $isEchoCancellationEnabled") - } else { - Log.e(TAG, "创建回音消除器失败") - } - } else { - Log.e(TAG, "无效的音频会话 ID") - } - } else { - Log.d(TAG, "设备不支持回音消除") - } - } catch (e: Exception) { - Log.e(TAG, "设置回音消除时出错: ${e.message}", e) - } - } - - // 释放回音消除资源 - private fun releaseEchoCancellation() { - try { - if (echoCanceler != null) { - echoCanceler?.enabled = false - echoCanceler?.release() - echoCanceler = null - isEchoCancellationEnabled = false - audioSessionId = -1 - Log.d(TAG, "回音消除资源已释放") - } - } catch (e: Exception) { - Log.e(TAG, "释放回音消除资源时出错: ${e.message}", e) - } - } - - // 设置回音消除开关 - fun setEchoCancellationEnabled(enabled: Boolean): Boolean { - try { - if (echoCanceler != null) { - echoCanceler?.enabled = enabled - isEchoCancellationEnabled = echoCanceler?.enabled == true - Log.d(TAG, "回音消除状态已设置为: $isEchoCancellationEnabled") - return isEchoCancellationEnabled == enabled - } else if (enabled) { - // 如果当前没有回音消除器但需要启用,尝试创建 - if (audioSessionId != -1) { - setupEchoCancellation() - return isEchoCancellationEnabled - } - } - return false - } catch (e: Exception) { - Log.e(TAG, "设置回音消除状态时出错: ${e.message}", e) - return false - } - } - - // 获取回音消除状态 - fun isEchoCancellationEnabled(): Boolean { - return isEchoCancellationEnabled - } - - // 设置支持的语言列表(用于自动语言检测) - fun setSupportedLanguages(languages: Array) { - if (languages.isNotEmpty()) { - Log.d(TAG, "设置支持的语言列表: ${languages.joinToString()}") - - // 如果当前是自动检测模式且识别器已初始化,需要重新初始化以应用新的语言列表 - if (isAutoDetectLanguage && recognizer != null) { - Log.d(TAG, "自动检测模式下更新语言列表,重新初始化识别器") - initialize(subscriptionKey, serviceRegion, languages) - } else { - // 仅更新语言列表,不重新初始化 - supportedLanguages = languages - - // 如果不是自动检测模式且只有一种语言,更新当前语言 - if (!isAutoDetectLanguage && languages.size == 1) { - currentLanguage = languages[0] - } - } - } - } - - // 获取当前是否启用自动语言检测 - fun isAutoDetectLanguageEnabled(): Boolean { - return isAutoDetectLanguage - } - - // 启用自动语言检测并设置支持的语言列表 - fun enableAutoDetectLanguage(enable: Boolean, languages: Array? = null): Boolean { - // 准备语言列表 - val languagesToUse = if (languages != null && languages.isNotEmpty()) { - // 如果提供了新的语言列表,使用新的列表 - languages - } else if (enable && supportedLanguages.size < 2) { - // 如果要启用自动检测但当前支持的语言少于2种,使用默认的两种语言 - arrayOf("zh-CN", "en-US") - } else { - // 否则使用当前的语言列表 - supportedLanguages - } - - // 如果要禁用自动检测,但提供了多种语言,只使用第一种语言 - val finalLanguages = if (!enable && languagesToUse.size > 1) { - arrayOf(languagesToUse[0]) - } else { - languagesToUse - } - - Log.d(TAG, "设置自动语言检测: enable=$enable, languages=${finalLanguages.joinToString()}") - - // 重新初始化以应用新的设置 - return initialize(subscriptionKey, serviceRegion, finalLanguages) - } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt index b05e1a442..287529fdc 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt @@ -5,7 +5,7 @@ import android.media.AudioAttributes import android.media.AudioDeviceInfo import android.media.AudioManager import android.os.Build -import android.util.Log +import com.example.deep_voice.core.utils.FileLogger import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.audio.* import java.util.concurrent.Future @@ -17,12 +17,11 @@ import java.util.concurrent.Semaphore * 该类封装了微软语音 SDK 的 TTS 功能,提供简单的接口供 Flutter 调用 * 基于微软官方文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis */ -class AzureTtsHelper { +class AzureTtsHelper(private val context: Context) { private val TAG = "AzureTtsHelper" private var speechConfig: SpeechConfig? = null private var synthesizer: SpeechSynthesizer? = null private var isInitialized = false - private var context: Context? = null private var audioManager: AudioManager? = null // 音频输出设备类型 @@ -50,14 +49,11 @@ class AzureTtsHelper { * @param subscriptionKey Azure 语音服务订阅密钥 * @param serviceRegion Azure 语音服务区域 * @param language 可选,默认语言,默认为 "zh-CN" - * @param context Android 上下文,用于获取音频服务 */ - fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN", context: Context? = null): Boolean { + fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean { try { - this.context = context - if (context != null) { - audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - } + audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + // 创建语音配置 speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) @@ -82,7 +78,7 @@ class AzureTtsHelper { synthesizer?.SynthesisCanceled?.addEventListener { _, _ -> semaphore.release() } isInitialized = true - Log.d(TAG, "TTS 引擎初始化成功") + FileLogger.d(TAG, "TTS 引擎初始化成功") // 设置默认音频输出类型为自动 if (audioManager != null) { @@ -91,7 +87,7 @@ class AzureTtsHelper { return true } catch (e: Exception) { - Log.e(TAG, "TTS 引擎初始化失败: ${e.message}") + FileLogger.e(TAG, "TTS 引擎初始化失败: ${e.message}, ${serviceRegion}") e.printStackTrace() return false } @@ -105,12 +101,12 @@ class AzureTtsHelper { */ fun setAudioOutputType(outputType: AudioOutputType): Boolean { if (!isInitialized) { - Log.e(TAG, "TTS 引擎尚未初始化") + FileLogger.e(TAG, "TTS 引擎尚未初始化") return false } if (audioManager == null) { - Log.e(TAG, "AudioManager 未初始化,无法设置音频输出设备") + FileLogger.e(TAG, "AudioManager 未初始化,无法设置音频输出设备") return false } @@ -122,13 +118,13 @@ class AzureTtsHelper { // 使用扬声器 audioManager?.mode = AudioManager.MODE_NORMAL audioManager?.isSpeakerphoneOn = true - Log.d(TAG, "已设置音频输出设备为扬声器") + FileLogger.d(TAG, "已设置音频输出设备为扬声器") } AudioOutputType.EARPIECE -> { // 使用听筒 audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION audioManager?.isSpeakerphoneOn = false - Log.d(TAG, "已设置音频输出设备为听筒") + FileLogger.d(TAG, "已设置音频输出设备为听筒") } AudioOutputType.AUTO -> { // 检查是否有耳机连接 @@ -136,12 +132,12 @@ class AzureTtsHelper { // 有耳机,使用耳机 audioManager?.mode = AudioManager.MODE_NORMAL audioManager?.isSpeakerphoneOn = false - Log.d(TAG, "已设置音频输出设备为耳机") + FileLogger.d(TAG, "已设置音频输出设备为耳机") } else { // 无耳机,使用听筒 audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION audioManager?.isSpeakerphoneOn = false - Log.d(TAG, "已设置音频输出设备为听筒") + FileLogger.d(TAG, "已设置音频输出设备为听筒") } } } @@ -151,7 +147,7 @@ class AzureTtsHelper { return true } catch (e: Exception) { - Log.e(TAG, "设置音频输出设备失败: ${e.message}") + FileLogger.e(TAG, "设置音频输出设备失败: ${e.message}") e.printStackTrace() return false } @@ -196,9 +192,9 @@ class AzureTtsHelper { synthesizer?.SynthesisCompleted?.addEventListener { _, _ -> semaphore.release() } synthesizer?.SynthesisCanceled?.addEventListener { _, _ -> semaphore.release() } - Log.d(TAG, "已重新创建语音合成器") + FileLogger.d(TAG, "已重新创建语音合成器") } catch (e: Exception) { - Log.e(TAG, "重新创建语音合成器失败: ${e.message}") + FileLogger.e(TAG, "重新创建语音合成器失败: ${e.message}") e.printStackTrace() } } @@ -210,10 +206,13 @@ class AzureTtsHelper { */ fun setVoice(voiceName: String): Boolean { if (!isInitialized) { - Log.e(TAG, "TTS 引擎尚未初始化") + FileLogger.e(TAG, "TTS 引擎尚未初始化") return false } - + if (voiceName == currentVoiceName) { + FileLogger.d(TAG, "已设置语音: $voiceName") + return true + } try { currentVoiceName = voiceName speechConfig?.setSpeechSynthesisVoiceName(voiceName) @@ -221,10 +220,10 @@ class AzureTtsHelper { // 重新创建合成器以应用新的语音设置 recreateSynthesizer() - Log.d(TAG, "已设置语音: $voiceName") + FileLogger.d(TAG, "已设置语音: $voiceName") return true } catch (e: Exception) { - Log.e(TAG, "设置语音失败: ${e.message}") + FileLogger.e(TAG, "设置语音失败: ${e.message}") e.printStackTrace() return false } @@ -239,7 +238,7 @@ class AzureTtsHelper { */ fun setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100): Boolean { if (!isInitialized) { - Log.e(TAG, "TTS 引擎尚未初始化") + FileLogger.e(TAG, "TTS 引擎尚未初始化") return false } @@ -248,10 +247,10 @@ class AzureTtsHelper { currentPitch = if (pitch == 0) "0%" else "${(pitch * 0.5).toInt()}%" currentVolume = "${volume.coerceIn(0, 100)}%" - Log.d(TAG, "已设置语音参数: 语速=$currentSpeechRate, 音调=$currentPitch, 音量=$currentVolume") + FileLogger.d(TAG, "已设置语音参数: 语速=$currentSpeechRate, 音调=$currentPitch, 音量=$currentVolume") return true } catch (e: Exception) { - Log.e(TAG, "设置语音参数失败: ${e.message}") + FileLogger.e(TAG, "设置语音参数失败: ${e.message}") e.printStackTrace() return false } @@ -270,12 +269,8 @@ class AzureTtsHelper { } try { - Log.d(TAG, "开始合成文本: $text") + FileLogger.d(TAG, "开始合成文本: $text") - // 如果设置为自动模式,检查耳机状态并更新设置 - if (currentAudioOutputType == AudioOutputType.AUTO) { - setAudioOutputType(AudioOutputType.AUTO) - } // 生成 SSML val ssml = generateSsml(text) @@ -283,7 +278,7 @@ class AzureTtsHelper { // 使用 SSML 合成语音 speakSsml(ssml, callback) } catch (e: Exception) { - Log.e(TAG, "语音合成异常: ${e.message}") + FileLogger.e(TAG, "语音合成异常: ${e.message}") e.printStackTrace() callback.onError("语音合成异常: ${e.message}") } @@ -320,7 +315,7 @@ class AzureTtsHelper { } try { - Log.d(TAG, "开始合成 SSML") + FileLogger.d(TAG, "开始合成 SSML") // 重置信号量 semaphore.drainPermits() @@ -339,29 +334,29 @@ class AzureTtsHelper { when (result.reason) { ResultReason.SynthesizingAudioCompleted -> { - Log.d(TAG, "语音合成完成,音频长度: ${result.audioLength / 10000}ms,音频数据大小: ${result.audioData?.size ?: 0} 字节") + FileLogger.d(TAG, "语音合成完成,音频长度: ${result.audioLength / 10000}ms,音频数据大小: ${result.audioData?.size ?: 0} 字节") callback.onSuccess("语音合成完成") } ResultReason.Canceled -> { val cancellation = SpeechSynthesisCancellationDetails.fromResult(result) - Log.e(TAG, "语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}") + FileLogger.e(TAG, "语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}") callback.onError("语音合成取消: ${cancellation.reason}, ${cancellation.errorDetails}") } else -> { - Log.e(TAG, "语音合成失败: ${result.reason}") + FileLogger.e(TAG, "语音合成失败: ${result.reason}") callback.onError("语音合成失败: ${result.reason}") } } result.close() } catch (e: Exception) { - Log.e(TAG, "处理语音合成结果异常: ${e.message}") + FileLogger.e(TAG, "处理语音合成结果异常: ${e.message}") e.printStackTrace() callback.onError("处理语音合成结果异常: ${e.message}") } }.start() } catch (e: Exception) { - Log.e(TAG, "语音合成异常: ${e.message}") + FileLogger.e(TAG, "语音合成异常: ${e.message}") e.printStackTrace() callback.onError("语音合成异常: ${e.message}") } @@ -372,17 +367,17 @@ class AzureTtsHelper { */ fun stopSpeaking(): Boolean { if (!isInitialized) { - Log.e(TAG, "TTS 引擎尚未初始化") + FileLogger.e(TAG, "TTS 引擎尚未初始化") return false } try { synthesizer?.StopSpeakingAsync() - Log.d(TAG, "已停止语音合成") + FileLogger.d(TAG, "已停止语音合成") return true } catch (e: Exception) { - Log.e(TAG, "停止语音合成失败: ${e.message}") + FileLogger.e(TAG, "停止语音合成失败: ${e.message}") e.printStackTrace() return false } @@ -404,9 +399,9 @@ class AzureTtsHelper { } isInitialized = false - Log.d(TAG, "TTS 引擎已释放") + FileLogger.d(TAG, "TTS 引擎已释放") } catch (e: Exception) { - Log.e(TAG, "释放 TTS 引擎失败: ${e.message}") + FileLogger.e(TAG, "释放 TTS 引擎失败: ${e.message}") e.printStackTrace() } } diff --git a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt index 968652ae1..63f10c57b 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt @@ -17,6 +17,9 @@ import android.os.Handler import android.os.Looper import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.example.deep_voice.core.utils.FileLogger class MainActivity: FlutterActivity() { @@ -140,15 +143,112 @@ class MainActivity: FlutterActivity() { var azureSpeechKey: String = "" var azureSpeechRegion: String = "" var volcanoAiApiKey: String = "" + + // 安全存储相关常量 + private const val SECURE_PREFS_FILENAME = "deep_voice_secure_prefs" + private const val KEY_AZURE_SPEECH_KEY = "azure_speech_key" + private const val KEY_AZURE_SPEECH_REGION = "azure_speech_region" + private const val KEY_VOLCANO_AI_API_KEY = "volcano_ai_api_key" + + // 会话管理 + private const val KEY_SESSION_ID = "session_id" + private var currentSessionId = "" + + /** + * 将密钥安全地存储到 EncryptedSharedPreferences + */ + fun saveKeysToSecureStorage(context: Context) { + try { + // 创建或获取主密钥 + val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + // 创建 EncryptedSharedPreferences + val sharedPreferences = EncryptedSharedPreferences.create( + context, + SECURE_PREFS_FILENAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + + // 生成新的会话ID + if (currentSessionId.isEmpty()) { + currentSessionId = java.util.UUID.randomUUID().toString() + } + + // 保存密钥和会话ID + sharedPreferences.edit() + .putString(KEY_AZURE_SPEECH_KEY, azureSpeechKey) + .putString(KEY_AZURE_SPEECH_REGION, azureSpeechRegion) + .putString(KEY_VOLCANO_AI_API_KEY, volcanoAiApiKey) + .putString(KEY_SESSION_ID, currentSessionId) + .apply() + + FileLogger.d("MainActivity", "密钥已安全保存到加密存储中") + } catch (e: Exception) { + FileLogger.e("MainActivity", "保存密钥到加密存储失败: ${e.message}") + e.printStackTrace() + } + } + + /** + * 从 EncryptedSharedPreferences 获取密钥 + */ + fun loadKeysFromSecureStorage(context: Context): Boolean { + try { + // 创建或获取主密钥 + val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + // 获取 EncryptedSharedPreferences + val sharedPreferences = EncryptedSharedPreferences.create( + context, + SECURE_PREFS_FILENAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + + // 检查会话ID是否匹配 + val savedSessionId = sharedPreferences.getString(KEY_SESSION_ID, "") ?: "" + + // 如果会话ID为空或不匹配,说明是新会话或应用被重启 + if (savedSessionId.isEmpty() || currentSessionId.isEmpty() || savedSessionId != currentSessionId) { + // 会话不匹配,但尝试加载密钥,不返回错误 + FileLogger.d("MainActivity", "会话ID不匹配,但尝试加载密钥") + // 设置当前会话ID为保存的会话ID + currentSessionId = savedSessionId + } + + // 加载密钥 + azureSpeechKey = sharedPreferences.getString(KEY_AZURE_SPEECH_KEY, "") ?: "" + azureSpeechRegion = sharedPreferences.getString(KEY_AZURE_SPEECH_REGION, "") ?: "" + volcanoAiApiKey = sharedPreferences.getString(KEY_VOLCANO_AI_API_KEY, "") ?: "" + + FileLogger.d("MainActivity", "已从加密存储加载密钥") + + // 检查是否成功获取所有密钥 + return azureSpeechKey.isNotEmpty() && azureSpeechRegion.isNotEmpty() && volcanoAiApiKey.isNotEmpty() + } catch (e: Exception) { + FileLogger.e("MainActivity", "从加密存储加载密钥失败: ${e.message}") + e.printStackTrace() + return false + } + } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // 初始化本地的 Azure 语音服务实例 - Log.d(TAG, "初始化本地的 Azure 语音服务实例") - azureAsrHelper = AzureAsrHelper() - azureTtsHelper = AzureTtsHelper() + // 初始化 FileLogger + FileLogger.init(applicationContext) + + // 初始化 Azure 语音服务 + azureAsrHelper = AzureAsrHelper(applicationContext) + azureTtsHelper = AzureTtsHelper(applicationContext) classicBluetoothHelper = ClassicBluetoothHelper(applicationContext) // 注册广播接收器 @@ -191,6 +291,10 @@ class MainActivity: FlutterActivity() { // 设置方法通道 setupMethodChannels(flutterEngine) + // 初始化 Azure 语音服务 + azureAsrHelper = AzureAsrHelper(this) + azureTtsHelper = AzureTtsHelper(this) + Log.d(TAG, "Flutter 引擎配置完成") } @@ -268,8 +372,6 @@ class MainActivity: FlutterActivity() { "initialize" -> { val subscriptionKey = call.argument("subscriptionKey") val region = call.argument("region") - val language = call.argument("language") ?: "zh-CN" - val autoDetectLanguage = call.argument("autoDetectLanguage") ?: false val supportedLanguages = call.argument>("supportedLanguages")?.toTypedArray() ?: arrayOf("zh-CN", "en-US") if (subscriptionKey == null || region == null) { @@ -285,9 +387,8 @@ class MainActivity: FlutterActivity() { } } "recognizeOnce" -> { - val language = call.argument("language") - azureAsrHelper.recognizeOnce(language, object : AzureAsrHelper.RecognizeCallback { + azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback { override fun onResult(text: String, detectedLanguage: String) { result.success(mapOf( "text" to text, @@ -375,37 +476,6 @@ class MainActivity: FlutterActivity() { azureAsrHelper.dispose() result.success(true) } - "enableAutoDetectLanguage" -> { - val enable = call.argument("enable") ?: false - val languages = call.argument>("languages")?.toTypedArray() - - try { - val success = azureAsrHelper.enableAutoDetectLanguage(enable, languages) - result.success(success) - } catch (e: Exception) { - result.error("AUTO_DETECT_ERROR", e.message, null) - } - } - - "setSupportedLanguages" -> { - val languages = call.argument>("languages")?.toTypedArray() - - if (languages == null || languages.isEmpty()) { - result.error("INVALID_ARGUMENTS", "语言列表不能为空", null) - return@setMethodCallHandler - } - - try { - azureAsrHelper.setSupportedLanguages(languages) - result.success(true) - } catch (e: Exception) { - result.error("SET_LANGUAGES_ERROR", e.message, null) - } - } - - "isAutoDetectLanguageEnabled" -> { - result.success(azureAsrHelper.isAutoDetectLanguageEnabled()) - } else -> { result.notImplemented() } @@ -416,21 +486,12 @@ class MainActivity: FlutterActivity() { MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "initialize" -> { - val subscriptionKey = call.argument("subscriptionKey") - val region = call.argument("serviceRegion") + val subscriptionKey = call.argument("subscriptionKey") ?: "" + val region = call.argument("region") ?: "" val language = call.argument("language") ?: "zh-CN" - if (subscriptionKey == null || region == null) { - result.error("INVALID_ARGUMENTS", "订阅密钥和区域不能为空", null) - return@setMethodCallHandler - } - - try { - val success = azureTtsHelper.initialize(subscriptionKey, region, language, this) - result.success(success) - } catch (e: Exception) { - result.error("INITIALIZATION_ERROR", e.message, null) - } + val success = azureTtsHelper.initialize(subscriptionKey, region, language) + result.success(success) } "setVoice" -> { val voiceName = call.argument("voiceName") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "语音名称不能为空", null) @@ -601,7 +662,7 @@ class MainActivity: FlutterActivity() { * 启动语音交互服务(带配置参数) */ private fun startVoiceInteractionService(call: MethodCall, result: MethodChannel.Result) { - Log.d(TAG, "启动语音交互服务") + FileLogger.d(TAG, "启动语音交互服务") // 获取配置参数 val key = call.argument("azure_speech_key") ?: "" @@ -613,6 +674,9 @@ class MainActivity: FlutterActivity() { azureSpeechRegion = region volcanoAiApiKey = aiKey + // 保存密钥到安全存储 + saveKeysToSecureStorage(applicationContext) + // 创建 Intent 并启动服务 val serviceIntent = Intent(this, VoiceInteractionService::class.java) @@ -623,6 +687,7 @@ class MainActivity: FlutterActivity() { startService(serviceIntent) } + // 返回成功结果 result.success(true) } @@ -716,6 +781,7 @@ class MainActivity: FlutterActivity() { } override fun onDestroy() { + Log.d(TAG, "onDestroy") // 注销所有广播接收器 try { unregisterReceiver(voiceInteractionReceiver) diff --git a/android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt b/android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt index a56c872cc..978f97e9c 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt @@ -25,6 +25,7 @@ import org.json.JSONArray import android.media.MediaPlayer import android.media.AudioAttributes import android.net.Uri +import com.example.deep_voice.core.utils.FileLogger /** * 后台语音交互 Service: @@ -70,6 +71,8 @@ class VoiceInteractionService : Service() { // 当前用户输入 private var currentUserInput = "" + + private var testKey = "" // 服务组件 private lateinit var mediaSession: MediaSessionCompat @@ -96,12 +99,14 @@ class VoiceInteractionService : Service() { 用户用语音和你交互. """.trimIndent() - // 添加无声媒体播放器 - private var silentPlayer: SilentMediaPlayer? = null + // 添加媒体播放器 + private var audioPlayer: MiniMediaPlayer? = null override fun onCreate() { super.onCreate() - Log.d(TAG, "onCreate - 语音交互服务正在创建") + // 初始化文件日志系统 + FileLogger.init(applicationContext) + FileLogger.d(TAG, "onCreate - 语音交互服务正在创建") // 初始化状态 isRunning.set(true) @@ -109,31 +114,25 @@ class VoiceInteractionService : Service() { // 初始化组件 audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - Log.d(TAG, "AudioManager初始化完成") + FileLogger.d(TAG, "AudioManager初始化完成") initServices() initMediaSession() registerMediaButtonReceiver() - - silentPlayer = SilentMediaPlayer(applicationContext) - silentPlayer?.start() - // Log.d(TAG, "无声播放器已启动") - + // 创建音频播放器 + audioPlayer = MiniMediaPlayer(applicationContext) + audioPlayer?.play(R.raw.silence, 0f) + // 设置为媒体播放状态 - setPlaybackState(PlaybackStateCompat.STATE_PLAYING) - Log.d(TAG, "设置播放状态为PLAYING") + // setPlaybackState(PlaybackStateCompat.STATE_PAUSED) + // FileLogger.d(TAG, "设置播放状态为STATE_PAUSED") // 启动监控和前台服务 startMonitoring() startForegroundServiceWithNotification() - Log.d(TAG, "VoiceInteractionService 初始化完成,等待蓝牙按键事件...") - - // 延迟3秒后测试按键处理功能 - // Handler(Looper.getMainLooper()).postDelayed({ - // testMediaButtonEvent() - // }, 3000) + FileLogger.d(TAG, "VoiceInteractionService 初始化完成,等待蓝牙按键事件...") } /** @@ -152,31 +151,56 @@ class VoiceInteractionService : Service() { */ private fun initServices() { // 创建新的Azure服务实例 - Log.d(TAG, "创建新的Azure服务实例") - azureAsrHelper = AzureAsrHelper() - azureTtsHelper = AzureTtsHelper() - - // 从 MainActivity 获取 Azure 配置 - val subscriptionKey = MainActivity.azureSpeechKey - val serviceRegion = MainActivity.azureSpeechRegion + FileLogger.d(TAG, "创建新的Azure服务实例") + azureAsrHelper = AzureAsrHelper(this) + azureTtsHelper = AzureTtsHelper(this) + + // 尝试从静态变量获取配置 + var subscriptionKey = MainActivity.azureSpeechKey + var serviceRegion = MainActivity.azureSpeechRegion + var volcanoKey = MainActivity.volcanoAiApiKey + + // 如果静态变量中没有配置,尝试从加密存储中加载 + if (subscriptionKey.isEmpty() || serviceRegion.isEmpty() || volcanoKey.isEmpty()) { + FileLogger.d(TAG, "静态变量中的配置信息不完整,尝试从加密存储加载") + + // 从加密存储加载密钥 + val keysLoaded = MainActivity.loadKeysFromSecureStorage(applicationContext) + + if (keysLoaded) { + // 更新本地变量 + subscriptionKey = MainActivity.azureSpeechKey + serviceRegion = MainActivity.azureSpeechRegion + volcanoKey = MainActivity.volcanoAiApiKey + + FileLogger.d(TAG, "已从加密存储加载配置信息") + } else { + FileLogger.e(TAG, "无法从加密存储加载配置信息") + } + } + // 初始化语音服务 if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) { // 初始化ASR azureAsrHelper.initialize(subscriptionKey, serviceRegion, arrayOf("zh-CN")) + // 初始化TTS azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") + + FileLogger.d(TAG, "Azure语音服务已初始化") } else { - Log.e(TAG, "Azure配置信息不完整,无法初始化Azure服务") + FileLogger.e(TAG, "Azure配置信息不完整,无法初始化Azure服务") } // 初始化火山AI服务 volcanoAIService = VolcanoAIService() - // 使用 MainActivity 中的 API key 初始化 - val volcanoApiKey = MainActivity.volcanoAiApiKey - if (volcanoApiKey.isNotEmpty()) { - volcanoAIService.initialize(volcanoApiKey) + + // 初始化火山AI服务 + if (volcanoKey.isNotEmpty()) { + volcanoAIService.initialize(volcanoKey) + FileLogger.d(TAG, "火山AI服务已初始化") } else { - Log.e(TAG, "火山AI配置信息不完整,无法初始化火山AI服务") + FileLogger.e(TAG, "火山AI配置信息不完整,无法初始化火山AI服务") } } @@ -184,7 +208,7 @@ class VoiceInteractionService : Service() { * 初始化 MediaSession */ private fun initMediaSession() { - Log.d(TAG, "开始初始化MediaSession") + FileLogger.d(TAG, "开始初始化MediaSession") mediaSession = MediaSessionCompat(applicationContext, "VoiceServiceSession") // 设置标志和状态 @@ -192,91 +216,24 @@ class VoiceInteractionService : Service() { MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS ) - Log.d(TAG, "MediaSession标志设置完成") + FileLogger.d(TAG, "MediaSession标志设置完成") // 设置回调 mediaSession.setCallback(object : MediaSessionCompat.Callback() { override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { - Log.d(TAG, "收到媒体按钮事件: $mediaButtonEvent") + FileLogger.d(TAG, "initMediaSession收到媒体按钮事件: $mediaButtonEvent") handleDoubleClick() - return true - - val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) - } else { - @Suppress("DEPRECATION") - mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent - } - - if (keyEvent != null) { - Log.d(TAG, "解析到按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}") - - if (keyEvent.action == KeyEvent.ACTION_DOWN) { - when (keyEvent.keyCode) { - KeyEvent.KEYCODE_MEDIA_PLAY -> { - Log.d(TAG, "检测到MEDIA_PLAY按键") - handleDoubleClick() - return true - } - KeyEvent.KEYCODE_MEDIA_PAUSE -> { - Log.d(TAG, "检测到MEDIA_PAUSE按键") - handleDoubleClick() - return true - } - KeyEvent.KEYCODE_HEADSETHOOK -> { - Log.d(TAG, "检测到HEADSETHOOK按键") - // 检测双击 - val currentTime = System.currentTimeMillis() - val timeDiff = currentTime - lastKeyEventTime - - Log.d(TAG, "按键时间差: $timeDiff ms, 当前计数: $keyEventCount") - - if (timeDiff < 500) { // 500毫秒内的按键视为连续按键 - keyEventCount++ - Log.d(TAG, "连续按键,计数增加到: $keyEventCount") - if (keyEventCount == 1) { // 这是第二次按键,即双击 - Log.d(TAG, "检测到双击,执行处理") - handleDoubleClick() - keyEventCount = 0 - return true - } - } else { - Log.d(TAG, "非连续按键,重置计数") - keyEventCount = 0 - } - lastKeyEventTime = currentTime - - // 单击也处理 - Log.d(TAG, "单击也处理") - handleDoubleClick() - return true - } - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { - Log.d(TAG, "检测到MEDIA_PLAY_PAUSE按键") - handleDoubleClick() - return true - } - else -> { - Log.d(TAG, "检测到其他按键: ${keyEvent.keyCode}") - } - } - } else { - Log.d(TAG, "非ACTION_DOWN事件: ${keyEvent.action}") - } - } else { - Log.d(TAG, "Intent中没有KeyEvent") - } - return super.onMediaButtonEvent(mediaButtonEvent) + return true } override fun onPlay() { - Log.d(TAG, "收到onPlay回调") + FileLogger.d(TAG, "收到onPlay回调") handleDoubleClick() return super.onPlay() } override fun onPause() { - Log.d(TAG, "收到onPause回调") + FileLogger.d(TAG, "收到onPause回调") handleDoubleClick() return super.onPause() } @@ -284,7 +241,7 @@ class VoiceInteractionService : Service() { // 激活 MediaSession mediaSession.isActive = true - Log.d(TAG, "MediaSession已激活") + FileLogger.d(TAG, "MediaSession已激活") } /** @@ -294,9 +251,9 @@ class VoiceInteractionService : Service() { try { val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver) - Log.d(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver") + FileLogger.d(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver") } catch (e: Exception) { - Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) + FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) } } @@ -330,7 +287,7 @@ class VoiceInteractionService : Service() { // 如果超过5秒没有检测到语音,且不在TTS播放中,暂停语音识别 if (!hasSpeechDetected && !isTtsSpeaking && elapsedTime >= RECOGNITION_TIMEOUT) { - Log.d(TAG, "超过${RECOGNITION_TIMEOUT/1000}秒未检测到语音,停止识别") + FileLogger.d(TAG, "超过${RECOGNITION_TIMEOUT/1000}秒未检测到语音,停止识别") isTimeoutPaused = true playNotification("没有听到您说话,已暂停对话。双击耳机按钮可重新开始。") @@ -348,13 +305,13 @@ class VoiceInteractionService : Service() { * 处理双击事件 */ private fun handleDoubleClick() { - Log.d(TAG, "处理双击事件") + FileLogger.d(TAG, "处理双击事件") //屏蔽短时间重复响应的问题 val currentTime = System.currentTimeMillis() val timeDiff = currentTime - lastKeyEventTime if (timeDiff < 500) { - Log.d(TAG, "短时间重复响应,忽略") + FileLogger.d(TAG, "短时间重复响应,忽略") return } lastKeyEventTime = currentTime @@ -363,42 +320,28 @@ class VoiceInteractionService : Service() { stopCurrentTTS() // 播放提示音 - playPrompt("我在!") + // playPrompt("我在!") + audioPlayer?.play(R.raw.listening) // 设置为播放状态 - setPlaybackState(PlaybackStateCompat.STATE_PLAYING) + // setPlaybackState(PlaybackStateCompat.STATE_PAUSED) // 重置超时暂停标志 isTimeoutPaused = false // 启动或重置语音识别 if (!isRecognitionActive) { - Log.d(TAG, "语音识别未激活,开始启动") + FileLogger.d(TAG, "语音识别未激活,开始启动") // 如果之前是因为超时暂停,重新初始化语音识别组件 - if (isTimeoutPaused) { - Log.d(TAG, "之前因超时暂停,重新初始化Azure服务") - // 释放旧的实例 - try { - azureAsrHelper.dispose() - } catch (e: Exception) { - Log.e(TAG, "释放旧的Azure ASR实例失败: ${e.message}", e) - } - - // 创建新的实例 - azureAsrHelper = AzureAsrHelper() - - // 重新初始化 - val subscriptionKey = MainActivity.azureSpeechKey - val serviceRegion = MainActivity.azureSpeechRegion - if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) { - azureAsrHelper.initialize(subscriptionKey, serviceRegion, arrayOf("zh-CN")) - } - } + // if (isTimeoutPaused) { + // Log.d(TAG, "之前因超时暂停,重新初始化Azure服务") + // restartAsr() + // } startVoiceRecognition() } else { - Log.d(TAG, "语音识别已激活,更新活动时间") + FileLogger.d(TAG, "语音识别已激活,更新活动时间") updateLastActivityTime() hasSpeechDetected = false } @@ -415,7 +358,7 @@ class VoiceInteractionService : Service() { playNotification("需要录音权限,请在设置中授予权限") return } - // 通知 Flutter 语音识别已启动 + // 通知 Flutter 语音识别已启动 notifyRecognitionStarted() isActive = true @@ -468,6 +411,7 @@ class VoiceInteractionService : Service() { }) } catch (e: Exception) { isRecognitionActive = false + FileLogger.e(TAG, "启动语音识别失败: ${e.message}", e) playNotification("启动语音识别失败") @@ -480,7 +424,7 @@ class VoiceInteractionService : Service() { private fun stopVoiceRecognition() { if (!isRecognitionActive) return - Log.d(TAG, "停止语音识别") + FileLogger.d(TAG, "停止语音识别") try { azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { @@ -489,19 +433,19 @@ class VoiceInteractionService : Service() { override fun onSessionStarted() {} override fun onSessionStopped() { isRecognitionActive = false - Log.d(TAG, "语音识别会话已停止") + FileLogger.d(TAG, "语音识别会话已停止") } override fun onCanceled(reason: String, errorDetails: String) { isRecognitionActive = false - Log.d(TAG, "语音识别已取消: $reason") + FileLogger.d(TAG, "语音识别已取消: $reason") } override fun onError(error: String) { isRecognitionActive = false - Log.e(TAG, "停止语音识别时出错: $error") + FileLogger.e(TAG, "停止语音识别时出错: $error") } }) } catch (e: Exception) { - Log.e(TAG, "停止语音识别异常: ${e.message}", e) + FileLogger.e(TAG, "停止语音识别异常: ${e.message}", e) // 确保状态一致性 isRecognitionActive = false } @@ -533,7 +477,7 @@ class VoiceInteractionService : Service() { // 同步聊天记录到Flutter端 notifyChatHistoryUpdated("personal_assistant", text, response) } catch (e: Exception) { - Log.e(TAG, "AI处理出错: ${e.message}") + FileLogger.e(TAG, "AI处理出错: ${e.message}") playNotification("AI处理出错") } }.start() @@ -689,11 +633,11 @@ class VoiceInteractionService : Service() { * 当 Service 被启动时调用 */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d(TAG, "onStartCommand: intent=$intent, action=${intent?.action}") + FileLogger.d(TAG, "onStartCommand: intent=$intent, action=${intent?.action}") // 处理媒体按键 Intent if (intent?.action == Intent.ACTION_MEDIA_BUTTON) { - Log.d(TAG, "收到媒体按钮Intent: ${intent.extras}") + FileLogger.d(TAG, "收到媒体按钮Intent: ${intent.extras}") val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) @@ -703,26 +647,26 @@ class VoiceInteractionService : Service() { } if (keyEvent != null) { - Log.d(TAG, "从Intent中解析到按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}") + FileLogger.d(TAG, "从Intent中解析到按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}") handleKeyEvent(keyEvent) } else { - Log.d(TAG, "Intent中没有按键事件,直接处理双击") + FileLogger.d(TAG, "Intent中没有按键事件,直接处理双击") handleDoubleClick() } } else if (intent?.action == ACTION_PAUSE_VOICE_INTERACTION) { // 处理来自 Flutter 的暂停请求 - Log.d(TAG, "收到暂停请求") + FileLogger.d(TAG, "收到暂停请求") pauseVoiceInteraction() } else { - Log.d(TAG, "收到其他Intent: action=${intent?.action}") + FileLogger.d(TAG, "收到其他Intent: action=${intent?.action}") } // 让 MediaSession 处理 Intent try { MediaButtonReceiver.handleIntent(mediaSession, intent) - Log.d(TAG, "MediaButtonReceiver处理Intent完成") + FileLogger.d(TAG, "MediaButtonReceiver处理Intent完成") } catch (e: Exception) { - Log.e(TAG, "MediaButtonReceiver处理Intent失败: ${e.message}", e) + FileLogger.e(TAG, "MediaButtonReceiver处理Intent失败: ${e.message}", e) } return START_STICKY @@ -732,50 +676,50 @@ class VoiceInteractionService : Service() { * 处理按键事件 */ private fun handleKeyEvent(keyEvent: KeyEvent) { - Log.d(TAG, "处理按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}") + FileLogger.d(TAG, "处理按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}") if (keyEvent.action == KeyEvent.ACTION_DOWN) { when (keyEvent.keyCode) { KeyEvent.KEYCODE_MEDIA_PLAY -> { - Log.d(TAG, "处理MEDIA_PLAY按键") + FileLogger.d(TAG, "处理MEDIA_PLAY按键") handleDoubleClick() } KeyEvent.KEYCODE_MEDIA_PAUSE -> { - Log.d(TAG, "处理MEDIA_PAUSE按键") + FileLogger.d(TAG, "处理MEDIA_PAUSE按键") handleDoubleClick() } KeyEvent.KEYCODE_HEADSETHOOK -> { - Log.d(TAG, "处理HEADSETHOOK按键") + FileLogger.d(TAG, "处理HEADSETHOOK按键") // 检测双击 val currentTime = System.currentTimeMillis() val timeDiff = currentTime - lastKeyEventTime - Log.d(TAG, "按键时间差: $timeDiff ms, 当前计数: $keyEventCount") + FileLogger.d(TAG, "按键时间差: $timeDiff ms, 当前计数: $keyEventCount") if (timeDiff < 500) { // 500毫秒内的按键视为连续按键 keyEventCount++ - Log.d(TAG, "连续按键,计数增加到: $keyEventCount") + FileLogger.d(TAG, "连续按键,计数增加到: $keyEventCount") if (keyEventCount == 1) { // 这是第二次按键,即双击 - Log.d(TAG, "检测到双击,执行处理") + FileLogger.d(TAG, "检测到双击,执行处理") handleDoubleClick() keyEventCount = 0 } } else { - Log.d(TAG, "非连续按键,重置计数") + FileLogger.d(TAG, "非连续按键,重置计数") keyEventCount = 0 } lastKeyEventTime = currentTime } KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { - Log.d(TAG, "处理MEDIA_PLAY_PAUSE按键") + FileLogger.d(TAG, "处理MEDIA_PLAY_PAUSE按键") handleDoubleClick() } else -> { - Log.d(TAG, "处理其他按键: ${keyEvent.keyCode}") + FileLogger.d(TAG, "处理其他按键: ${keyEvent.keyCode}") } } } else { - Log.d(TAG, "非ACTION_DOWN事件: ${keyEvent.action}") + FileLogger.d(TAG, "非ACTION_DOWN事件: ${keyEvent.action}") } } @@ -783,11 +727,11 @@ class VoiceInteractionService : Service() { override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() { - Log.d(TAG, "onDestroy - 语音交互服务正在销毁") + FileLogger.d(TAG, "onDestroy - 语音交互服务正在销毁") - // 停止无声播放器 - silentPlayer?.release() - silentPlayer = null + // 释放音频播放器 + audioPlayer?.release() + audioPlayer = null // 停止监控 stopMonitoring() @@ -804,13 +748,16 @@ class VoiceInteractionService : Service() { mediaSession.release() // 释放Azure服务实例 - Log.d(TAG, "释放Azure服务实例") + FileLogger.d(TAG, "释放Azure服务实例") azureAsrHelper.dispose() azureTtsHelper.dispose() // 更新服务状态 isRunning.set(false) + // 关闭日志系统 + FileLogger.shutdown() + super.onDestroy() } @@ -819,7 +766,7 @@ class VoiceInteractionService : Service() { * 用于从 Flutter 端控制服务 */ private fun pauseVoiceInteraction() { - Log.d(TAG, "暂停语音交互(来自Flutter的请求)") + FileLogger.d(TAG, "暂停语音交互(来自Flutter的请求)") // 停止当前TTS播放 stopCurrentTTS() @@ -838,9 +785,9 @@ class VoiceInteractionService : Service() { * 通知 Flutter 端聊天记录已更新 */ private fun notifyChatHistoryUpdated(agentId: String, userMessage: String, assistantMessage: String) { - Log.d(TAG, "通知Flutter聊天记录已更新: agentId=$agentId") - Log.d(TAG, "用户消息: ${userMessage.take(50)}...") - Log.d(TAG, "助手回复: ${assistantMessage.take(50)}...") + FileLogger.d(TAG, "通知Flutter聊天记录已更新: agentId=$agentId") + FileLogger.d(TAG, "用户消息: ${userMessage.take(50)}...") + FileLogger.d(TAG, "助手回复: ${assistantMessage.take(50)}...") // 创建广播 Intent val intent = Intent(ACTION_CHAT_HISTORY_UPDATED).apply { @@ -852,14 +799,14 @@ class VoiceInteractionService : Service() { // 发送广播 sendBroadcast(intent) - Log.d(TAG, "已发送聊天记录广播") + FileLogger.d(TAG, "已发送聊天记录广播") } /** * 通知 Flutter 端语音识别已启动 */ private fun notifyRecognitionStarted() { - Log.d(TAG, "通知Flutter语音识别已启动") + FileLogger.d(TAG, "通知Flutter语音识别已启动") // 创建广播 Intent val intent = Intent(ACTION_RECOGNITION_STARTED).apply { @@ -871,162 +818,77 @@ class VoiceInteractionService : Service() { sendBroadcast(intent) } - /** - * 测试媒体按钮事件处理 - */ - private fun testMediaButtonEvent() { - Log.d(TAG, "开始测试媒体按钮事件处理") - - try { - // 创建一个模拟的HEADSETHOOK按键事件 - val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK) - - // 创建一个包含按键事件的Intent - val intent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { - putExtra(Intent.EXTRA_KEY_EVENT, keyEvent) - } - - // 直接调用onMediaButtonEvent方法 - Log.d(TAG, "直接调用onMediaButtonEvent方法") - val result = mediaSession.controller.dispatchMediaButtonEvent(keyEvent) - Log.d(TAG, "dispatchMediaButtonEvent结果: $result") - - // 通过MediaButtonReceiver发送Intent - Log.d(TAG, "通过MediaButtonReceiver发送Intent") - MediaButtonReceiver.handleIntent(mediaSession, intent) - - // 直接调用handleKeyEvent方法 - Log.d(TAG, "直接调用handleKeyEvent方法") - handleKeyEvent(keyEvent) - - Log.d(TAG, "测试媒体按钮事件处理完成") - } catch (e: Exception) { - Log.e(TAG, "测试媒体按钮事件处理失败: ${e.message}", e) - } - } /** - * 无声媒体播放器类 - * 用于播放一个1秒的静音音频,确保系统认为app在实际播放声音 + * 简易媒体播放器,用于播放raw中的音频资源 */ - inner class SilentMediaPlayer(private val context: Context) { + inner class MiniMediaPlayer(private val context: Context) { private var mediaPlayer: MediaPlayer? = null - private var isPlayingAudio = false - - init { - try { - setupMediaPlayer() - } catch (e: Exception) { - Log.e(TAG, "初始化无声播放器失败: ${e.message}", e) - } - } - private fun setupMediaPlayer() { + /** + * 播放音频资源 + * @param resId 资源ID (R.raw.xxx) + * @param volume 音量 0.0-1.0 + */ + fun play(resId: Int, volume: Float = 1.0f) { try { - // 创建一个MediaPlayer - mediaPlayer = MediaPlayer() - - // 设置一个自带的静音资源 - val afd = context.resources.openRawResourceFd(R.raw.silence) - if (afd != null) { - mediaPlayer?.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) - afd.close() - } else { - // 如果没有资源,尝试使用一个虚拟数据源 - Log.w(TAG, "未找到静音资源,尝试使用虚拟数据源") - mediaPlayer?.setDataSource(context, Uri.parse("android.resource://${context.packageName}/raw/silence")) - } - - // 设置循环播放 - mediaPlayer?.isLooping = false - - // 设置音量为0 - mediaPlayer?.setVolume(0f, 0f) + // 释放之前的资源 + release() - // 设置音频特性 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val audioAttributes = AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_MEDIA) - .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .build() - mediaPlayer?.setAudioAttributes(audioAttributes) - } else { - @Suppress("DEPRECATION") - mediaPlayer?.setAudioStreamType(AudioManager.STREAM_MUSIC) - } - - // 准备播放 - mediaPlayer?.prepare() - - // 设置完成监听器 - 虽然设置了循环播放,但以防万一 - mediaPlayer?.setOnCompletionListener { - Log.d(TAG, "无声播放完成") - isPlayingAudio = false - release() - // start() - } - - // 设置错误监听器 - mediaPlayer?.setOnErrorListener { _, what, extra -> - Log.e(TAG, "无声播放错误: what=$what, extra=$extra") - isPlayingAudio = false - // 尝试重新创建 - setupMediaPlayer() - true - } - - Log.d(TAG, "无声播放器初始化成功") - } catch (e: Exception) { - Log.e(TAG, "设置无声播放器失败: ${e.message}", e) - mediaPlayer?.release() - mediaPlayer = null - } - } - - fun start() { - try { - if (mediaPlayer == null) { - setupMediaPlayer() - } - - if (mediaPlayer != null && !isPlayingAudio) { - mediaPlayer?.start() - isPlayingAudio = true - Log.d(TAG, "无声播放开始") + // 创建播放器并设置资源 + mediaPlayer = MediaPlayer().apply { + // 设置音频资源 + context.resources.openRawResourceFd(resId)?.use { fd -> + setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) + } + + // 设置音量 + setVolume(volume.coerceIn(0f, 1f), volume.coerceIn(0f, 1f)) + + // 播放完成后自动释放 + setOnCompletionListener { release() } + + // 准备并播放 + prepare() + start() } } catch (e: Exception) { - Log.e(TAG, "开始无声播放失败: ${e.message}", e) - + release() } } + /** + * 停止播放 + */ fun stop() { try { - if (mediaPlayer != null && isPlayingAudio) { - mediaPlayer?.pause() - isPlayingAudio = false - Log.d(TAG, "无声播放停止") + mediaPlayer?.apply { + if (isPlaying) stop() } } catch (e: Exception) { - Log.e(TAG, "停止无声播放失败: ${e.message}", e) + // 忽略异常 } } + /** + * 释放资源 + */ fun release() { try { - mediaPlayer?.stop() - mediaPlayer?.release() + mediaPlayer?.apply { + if (isPlaying) stop() + release() + } mediaPlayer = null - isPlayingAudio = false - Log.d(TAG, "无声播放器已释放") } catch (e: Exception) { - Log.e(TAG, "释放无声播放器失败: ${e.message}", e) + // 忽略异常 } } - fun isPlaying(): Boolean { - return isPlayingAudio && mediaPlayer != null - } + /** + * 是否正在播放 + */ + fun isPlaying() = mediaPlayer?.isPlaying == true } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/core/utils/FileLogger.kt b/android/app/src/main/kotlin/com/example/deep_voice/core/utils/FileLogger.kt new file mode 100644 index 000000000..9d32a81b8 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/deep_voice/core/utils/FileLogger.kt @@ -0,0 +1,237 @@ +package com.example.deep_voice.core.utils + +import android.content.Context +import android.util.Log +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** + * 文件日志工具 + * 将日志同时输出到Logcat和文件系统 + */ +class FileLogger private constructor(context: Context) { + private val logDir: File + private var currentLogFile: File? = null + private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + private val timeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()) + private val logExecutor: ExecutorService = Executors.newSingleThreadExecutor() + + init { + // 初始化日志目录 + logDir = File(context.getExternalFilesDir(null), "logs") + if (!logDir.exists()) { + logDir.mkdirs() + } + + // 创建当天的日志文件 + updateLogFile() + } + + /** + * 更新日志文件为当天日期 + */ + private fun updateLogFile() { + val today = dateFormat.format(Date()) + val logFile = File(logDir, "log_$today.txt") + + if (currentLogFile == null || currentLogFile?.absolutePath != logFile.absolutePath) { + currentLogFile = logFile + + // 如果是新文件,添加文件头 + if (!logFile.exists()) { + try { + logFile.createNewFile() + FileWriter(logFile, true).use { writer -> + writer.append("===== 日志开始: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())} =====\n") + } + } catch (e: IOException) { + Log.e(TAG, "创建日志文件失败: ${e.message}") + } + } + } + } + + /** + * 调试级别日志 + */ + fun d(tag: String, message: String) { + // 输出到Logcat + Log.d(tag, message) + + // 异步写入文件 + logToFile("D", tag, message) + } + + /** + * 信息级别日志 + */ + fun i(tag: String, message: String) { + // 输出到Logcat + Log.i(tag, message) + + // 异步写入文件 + logToFile("I", tag, message) + } + + /** + * 警告级别日志 + */ + fun w(tag: String, message: String) { + // 输出到Logcat + Log.w(tag, message) + + // 异步写入文件 + logToFile("W", tag, message) + } + + /** + * 错误级别日志 + */ + fun e(tag: String, message: String, throwable: Throwable? = null) { + // 输出到Logcat + if (throwable != null) { + Log.e(tag, message, throwable) + } else { + Log.e(tag, message) + } + + // 异步写入文件 + val errorMsg = if (throwable != null) { + "$message\n${throwable.message}\n${throwable.stackTraceToString()}" + } else { + message + } + + logToFile("E", tag, errorMsg) + } + + /** + * 将日志写入文件 + */ + private fun logToFile(level: String, tag: String, message: String) { + return + logExecutor.execute { + try { + // 检查是否需要更新日志文件 + updateLogFile() + + // 格式化日志内容 + val timestamp = timeFormat.format(Date()) + val logLine = "$timestamp $level/$tag: $message\n" + + // 写入文件 + if (currentLogFile != null && currentLogFile!!.exists()) { + FileWriter(currentLogFile!!, true).use { writer -> + writer.append(logLine) + } + } + } catch (e: IOException) { + Log.e(TAG, "写入日志文件失败: ${e.message}") + } + } + } + + /** + * 清理旧日志文件 + * @param daysToKeep 保留的天数 + */ + fun cleanOldLogs(daysToKeep: Int = 7) { + logExecutor.execute { + try { + val currentTime = System.currentTimeMillis() + val maxAge = daysToKeep * 24 * 60 * 60 * 1000L + + logDir.listFiles()?.forEach { file -> + if (file.isFile && file.name.startsWith("log_") && file.name.endsWith(".txt")) { + val fileAge = currentTime - file.lastModified() + if (fileAge > maxAge) { + val deleted = file.delete() + if (deleted) { + Log.d(TAG, "已删除旧日志文件: ${file.name}") + } + } + } + } + } catch (e: Exception) { + Log.e(TAG, "清理旧日志失败: ${e.message}") + } + } + } + + /** + * 关闭日志系统 + */ + fun shutdown() { + logExecutor.shutdown() + } + + companion object { + private const val TAG = "FileLogger" + private var INSTANCE: FileLogger? = null + + /** + * 初始化日志系统 + */ + @Synchronized + fun init(context: Context) { + if (INSTANCE == null) { + INSTANCE = FileLogger(context.applicationContext) + d(TAG, "文件日志系统已初始化") + } + } + + /** + * 调试日志 + */ + fun d(tag: String, message: String) { + INSTANCE?.d(tag, message) ?: Log.d(tag, message) + } + + /** + * 信息日志 + */ + fun i(tag: String, message: String) { + INSTANCE?.i(tag, message) ?: Log.i(tag, message) + } + + /** + * 警告日志 + */ + fun w(tag: String, message: String) { + INSTANCE?.w(tag, message) ?: Log.w(tag, message) + } + + /** + * 错误日志 + */ + fun e(tag: String, message: String, throwable: Throwable? = null) { + INSTANCE?.e(tag, message, throwable) ?: run { + if (throwable != null) { + Log.e(tag, message, throwable) + } else { + Log.e(tag, message) + } + } + } + + /** + * 清理旧日志 + */ + fun cleanOldLogs(daysToKeep: Int = 7) { + INSTANCE?.cleanOldLogs(daysToKeep) + } + + /** + * 关闭日志系统 + */ + fun shutdown() { + INSTANCE?.shutdown() + INSTANCE = null + } + } +} \ No newline at end of file diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 9327bdb58..e6ec09174 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -37,6 +37,6 @@ class InitialBinding extends Bindings { Get.lazyPut(() => VoiceInteractionService(), fenix: true); // 经典蓝牙服务 - Get.put(ClassicBluetoothService()..initialize(), permanent: true); + Get.lazyPut(() => ClassicBluetoothService(), fenix: true); } } \ No newline at end of file diff --git a/lib/data/services/azure_asr_service.dart b/lib/data/services/azure_asr_service.dart index c24ebd480..e3ddb78d7 100644 --- a/lib/data/services/azure_asr_service.dart +++ b/lib/data/services/azure_asr_service.dart @@ -15,13 +15,7 @@ class AzureAsrService { late final String _subscriptionKey; late final String _serviceRegion; - // 当前语言 - String _currentLanguage = 'zh-CN'; - String get currentLanguage => _currentLanguage; - - // 自动语言检测相关 - bool _isAutoDetectLanguageEnabled = false; - bool get isAutoDetectLanguageEnabled => _isAutoDetectLanguageEnabled; + List _supportedLanguages = ['zh-CN', 'en-US']; List get supportedLanguages => _supportedLanguages; @@ -102,50 +96,35 @@ class AzureAsrService { /// /// 返回 true 表示初始化成功,否则抛出 PlatformException Future initialize({ - String language = 'zh-CN', - bool autoDetectLanguage = false, List? supportedLanguages, }) async { - _currentLanguage = language; - _isAutoDetectLanguageEnabled = autoDetectLanguage; - - if (supportedLanguages != null && supportedLanguages.isNotEmpty) { - _supportedLanguages = supportedLanguages; - } - - if (_isInitialized && - _currentLanguage == language && - _isAutoDetectLanguageEnabled == autoDetectLanguage) { - return true; - } - - if (_isInitialized) { - await dispose(); - } - try { + + if (supportedLanguages != null && supportedLanguages.isNotEmpty) { + _supportedLanguages = supportedLanguages; + } + + // 检查是否需要重新初始化 + if (_isInitialized) { + await dispose(); + } + final bool result = await _channel.invokeMethod('initialize', { 'subscriptionKey': _subscriptionKey, 'region': _serviceRegion, - 'language': language, - 'autoDetectLanguage': autoDetectLanguage, 'supportedLanguages': _supportedLanguages, }); + _isInitialized = result; + Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); return result; } catch (e) { - Logger.error('语音识别初始化失败: $e'); + Logger.error('Azure 语音识别服务初始化失败: $e'); _isInitialized = false; - throw e; + rethrow; } } - /// 设置识别语言 - /// - /// 注意:更改语言需要重新初始化识别器 - Future setRecognitionLanguage(String language) async { - return initialize(language: language); - } /// 执行一次性语音识别 /// @@ -156,9 +135,7 @@ class AzureAsrService { } try { - final result = await _channel.invokeMethod('recognizeOnce', { - 'language': _currentLanguage, - }); + final result = await _channel.invokeMethod('recognizeOnce'); if (result is Map) { final String text = result['text'] as String? ?? ''; @@ -175,7 +152,7 @@ class AzureAsrService { throw Exception('无效的识别结果格式'); } catch (e) { Logger.error('语音识别失败: $e'); - throw e; + rethrow; } } @@ -187,6 +164,11 @@ class AzureAsrService { await initialize(); } + // 确保事件通道已准备好 + if (!await _waitForEventChannel()) { + throw Exception('事件通道未准备好,无法开始连续识别'); + } + if (_isContinuousRecognitionActive) { await stopContinuousRecognition(); } @@ -194,6 +176,7 @@ class AzureAsrService { try { _eventStreamController = StreamController.broadcast(); + // 开始连续识别 final bool result = await _channel.invokeMethod('startContinuousRecognition'); if (!result) { @@ -206,7 +189,7 @@ class AzureAsrService { } catch (e) { Logger.error('开始连续语音识别失败: $e'); _cleanupEventStream(); - throw e; + rethrow; } } @@ -225,7 +208,7 @@ class AzureAsrService { } catch (e) { Logger.error('停止连续语音识别失败: $e'); _cleanupEventStream(); - throw e; + rethrow; } } @@ -324,68 +307,18 @@ class AzureAsrService { await stopContinuousRecognition(); } + // 通知原生端释放资源 await _channel.invokeMethod('dispose'); _isInitialized = false; + + Logger.info('Azure 语音识别资源已释放'); } catch (e) { Logger.error('释放语音识别资源失败: $e'); _cleanupEventStream(); _isInitialized = false; } } - - /// 启用或禁用自动语言检测 - Future enableAutoDetectLanguage(bool enable, {List? languages}) async { - if (!_isInitialized) { - await initialize(autoDetectLanguage: enable, supportedLanguages: languages); - return _isAutoDetectLanguageEnabled; - } - - try { - final bool result = await _channel.invokeMethod('enableAutoDetectLanguage', { - 'enable': enable, - 'languages': languages, - }); - - if (result) { - _isAutoDetectLanguageEnabled = enable; - if (languages != null && languages.isNotEmpty) { - _supportedLanguages = languages; - } - } - - return result; - } catch (e) { - Logger.error('设置自动语言检测失败: $e'); - throw e; - } - } - - /// 设置支持的语言列表(用于自动语言检测) - Future setSupportedLanguages(List languages) async { - if (languages.isEmpty) { - throw ArgumentError('语言列表不能为空'); - } - - if (!_isInitialized) { - await initialize(supportedLanguages: languages); - return true; - } - - try { - final bool result = await _channel.invokeMethod('setSupportedLanguages', { - 'languages': languages, - }); - - if (result) { - _supportedLanguages = languages; - } - - return result; - } catch (e) { - Logger.error('设置支持的语言列表失败: $e'); - throw e; - } - } + } /// 识别事件类型 diff --git a/lib/data/services/azure_tts_service.dart b/lib/data/services/azure_tts_service.dart index 005dc9b5e..4cd000f22 100644 --- a/lib/data/services/azure_tts_service.dart +++ b/lib/data/services/azure_tts_service.dart @@ -69,7 +69,8 @@ class AzureTtsService extends GetxService { _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; } - + + /// 初始化TTS服务 Future initialize({String language = 'zh-CN'}) async { if (_isInitialized) return true; @@ -82,7 +83,7 @@ class AzureTtsService extends GetxService { final result = await _channel.invokeMethod('initialize', { 'subscriptionKey': _subscriptionKey, - 'serviceRegion': _serviceRegion, + 'region': _serviceRegion, 'language': language, }); diff --git a/lib/data/services/classic_bluetooth_service.dart b/lib/data/services/classic_bluetooth_service.dart index 39e77f4bf..2c62801a8 100644 --- a/lib/data/services/classic_bluetooth_service.dart +++ b/lib/data/services/classic_bluetooth_service.dart @@ -162,15 +162,7 @@ class ClassicBluetoothService extends GetxService { try { Logger.info('正在初始化 ClassicBluetoothService...'); - // 检查通道是否可用 - try { - // 调用一个简单方法检查通道是否已设置 - await _channel.invokeMethod('isBluetoothEnabled'); - Logger.info('蓝牙方法通道可用'); - } catch (e) { - Logger.error('蓝牙方法通道不可用: $e'); - // 这里不要立即返回false,尝试继续设置其他内容 - } + // 设置事件监听器 _setupBluetoothStateListener(); diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index da5222328..fd4dc4772 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -418,8 +418,6 @@ class ChatController extends GetxController { _voiceInteractionService.pauseVoiceInteraction(); try { - // 初始化语音识别 - await _asrService.initialize(language: 'zh-CN'); // 启动连续语音识别 // 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据 diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 14318b870..0d71aa111 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -1,3 +1,4 @@ +import 'package:deep_voice/data/services/voice_interaction_service.dart'; import 'package:get/get.dart'; import 'dart:async'; import '../../../routes/app_routes.dart'; @@ -6,6 +7,7 @@ import '../../../core/utils/logger.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; + /// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程 class PairingController extends GetxController { static PairingController get to => Get.find(); @@ -52,41 +54,17 @@ class PairingController extends GetxController { _initialize(); } - @override - void onReady() { - super.onReady(); - // 设置连接监听器,确保在页面完全加载后设置,避免过早监听可能导致的问题 - _setupDeviceListeners(); - } - - /// 设置设备监听器 - void _setupDeviceListeners() { - try { - if (!_isBluetoothServiceAvailable.value) return; - - // 监听设备连接事件 - _bluetoothService.onDeviceConnected((device) { - Logger.info('检测到蓝牙设备连接: ${device.name}'); - - // 更新状态并跳转到主页 - statusDescription.value = '已连接到设备: ${device.name}'; - - // 立即跳转到主页 - _navigateToHome(); - }); - } catch (e) { - Logger.error('设置设备监听器失败: $e'); - } - } - - /// 立即跳转到主页 - void _navigateToHome() { - Get.offAllNamed(Routes.home); - } - + /// 完整的初始化流程 Future _initialize() async { try { + + VoiceInteractionService.to.initialize(); + VoiceInteractionService.to.startService(); + + + + isLoading.value = true; statusDescription.value = '正在初始化...'; @@ -102,6 +80,9 @@ class PairingController extends GetxController { return; } + // 设置连接监听器,确保在页面完全加载后设置,避免过早监听可能导致的问题 + _setupDeviceListeners(); + // 2. 检查权限 statusDescription.value = '正在检查权限...'; final hasPermission = await _bluetoothService.checkAndRequestPermissions(); @@ -133,6 +114,33 @@ class PairingController extends GetxController { isLoading.value = false; } } + + + /// 设置设备监听器 + void _setupDeviceListeners() { + try { + if (!_isBluetoothServiceAvailable.value) return; + + // 监听设备连接事件 + _bluetoothService.onDeviceConnected((device) { + Logger.info('检测到蓝牙设备连接: ${device.name}'); + + // 更新状态并跳转到主页 + statusDescription.value = '已连接到设备: ${device.name}'; + + // 立即跳转到主页 + _navigateToHome(); + }); + } catch (e) { + Logger.error('设置设备监听器失败: $e'); + } + } + + /// 立即跳转到主页 + void _navigateToHome() { + Get.offAllNamed(Routes.home); + } + /// 刷新设备 Future refreshConnectedDevices() async { diff --git a/lib/modules/test/controllers/azure_asr_test_controller.dart b/lib/modules/test/controllers/azure_asr_test_controller.dart index bcb390d49..ba60adcb1 100644 --- a/lib/modules/test/controllers/azure_asr_test_controller.dart +++ b/lib/modules/test/controllers/azure_asr_test_controller.dart @@ -54,9 +54,8 @@ class AzureAsrTestController extends GetxController { // 初始化ASR服务 Future _initializeAsr() async { try { + // 根据选择的支持语言初始化服务 final success = await _azureAsrService.initialize( - language: selectedLanguage.value, - autoDetectLanguage: isAutoDetectEnabled.value, supportedLanguages: selectedSupportedLanguages, ); asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; @@ -214,16 +213,25 @@ class AzureAsrTestController extends GetxController { selectedLanguage.value = language; + // 停止当前识别 if (_azureAsrService.isContinuousRecognitionActive()) { await stopContinuousRecognition(); } - final success = await _azureAsrService.setRecognitionLanguage(language); - asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; + // 确保新语言在选中支持的语言列表中 + if (!selectedSupportedLanguages.contains(language)) { + // 添加新语言到支持列表 + final newLanguages = [...selectedSupportedLanguages, language]; + selectedSupportedLanguages.value = newLanguages; + + // 重新初始化ASR服务 + await _initializeAsr(); + } recognizedText.value = ''; partialText.value = ''; detectedLanguage.value = ''; + asrState.value = AzureAsrState.initialized; } catch (e) { Logger.error('更改语言出错: $e'); asrState.value = AzureAsrState.error; @@ -235,16 +243,13 @@ class AzureAsrTestController extends GetxController { try { isAutoDetectEnabled.value = !isAutoDetectEnabled.value; + // 停止当前识别 if (_azureAsrService.isContinuousRecognitionActive()) { await stopContinuousRecognition(); } - final success = await _azureAsrService.enableAutoDetectLanguage( - isAutoDetectEnabled.value, - languages: selectedSupportedLanguages, - ); - - asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; + // 重新初始化ASR服务 - 自动语言检测现在通过选择多种语言实现 + await _initializeAsr(); recognizedText.value = ''; partialText.value = ''; @@ -262,11 +267,16 @@ class AzureAsrTestController extends GetxController { selectedSupportedLanguages.value = languages; + // 停止当前识别 if (_azureAsrService.isContinuousRecognitionActive()) { await stopContinuousRecognition(); } - final success = await _azureAsrService.setSupportedLanguages(languages); + // 重新初始化ASR服务,使用新的语言列表 + final success = await _azureAsrService.initialize( + supportedLanguages: languages, + ); + asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; } catch (e) { Logger.error('更新支持的语言列表出错: $e'); diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 038b5104c..0fd6d268b 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -126,13 +126,17 @@ class TranslationController extends GetxController { Future _initServices() async { try { await _translationService.initialize(); + await _ttsService.initialize(); + // 初始化 ASR 服务时,明确指定需要支持的语言 final List asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; - await _ttsService.initialize(); + Logger.info('初始化ASR服务,支持语言: $asrSupportedLanguages'); + await _asrService.initialize( - autoDetectLanguage: true, supportedLanguages: asrSupportedLanguages, ); + + Logger.info('所有服务初始化完成'); } catch (e) { Logger.error('初始化服务失败: $e'); } @@ -158,9 +162,14 @@ class TranslationController extends GetxController { _voiceInteractionService.pauseVoiceInteraction(); try { - // 更新ASR支持的语言 - final List asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; - await _asrService.setSupportedLanguages(asrSupportedLanguages); + // 每次开始识别前检查 ASR 服务是否已配置为当前语言对 + final List currentLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; + final bool needsReinit = !_asrService.supportedLanguages.toSet().containsAll(currentLanguages); + + if (needsReinit) { + Logger.info('识别开始前发现语言变更,重新初始化ASR服务'); + await _reinitializeAsrService(); + } currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); @@ -462,6 +471,9 @@ class TranslationController extends GetxController { // 保存语言设置 _saveLanguageSettings(); + + // 更新ASR支持的语言并重新初始化 + _reinitializeAsrService(); } // 设置源语言 @@ -478,6 +490,9 @@ class TranslationController extends GetxController { // 保存语言设置 _saveLanguageSettings(); + + // 更新ASR支持的语言并重新初始化 + _reinitializeAsrService(); } } @@ -495,6 +510,28 @@ class TranslationController extends GetxController { // 保存语言设置 _saveLanguageSettings(); + + // 更新ASR支持的语言并重新初始化 + _reinitializeAsrService(); + } + } + + // 重新初始化ASR服务以支持新的语言 + Future _reinitializeAsrService() async { + try { + // 更新ASR支持的语言 + final List asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; + + Logger.info('重新初始化ASR服务,支持语言: $asrSupportedLanguages'); + + // 重新初始化ASR服务 + await _asrService.initialize( + supportedLanguages: asrSupportedLanguages, + ); + + Logger.info('ASR服务重新初始化成功'); + } catch (e) { + Logger.error('重新初始化ASR服务失败: $e'); } }