|
|
|
@ -18,6 +18,8 @@ import android.support.v4.media.session.MediaSessionCompat |
|
|
|
import android.support.v4.media.session.PlaybackStateCompat |
|
|
|
import android.content.ComponentName |
|
|
|
import android.media.AudioManager |
|
|
|
import android.os.Handler |
|
|
|
import android.os.Looper |
|
|
|
import java.util.concurrent.atomic.AtomicBoolean |
|
|
|
import org.json.JSONArray |
|
|
|
|
|
|
|
@ -41,15 +43,50 @@ class VoiceInteractionService : Service() { |
|
|
|
fun isServiceRunning(): Boolean { |
|
|
|
return isRunning.get() |
|
|
|
} |
|
|
|
|
|
|
|
// 语音识别超时时间(毫秒) |
|
|
|
private const val RECOGNITION_TIMEOUT = 5000L |
|
|
|
|
|
|
|
// 服务状态变化广播 Action |
|
|
|
const val ACTION_SERVICE_STATUS_CHANGED = "com.example.deep_voice.ACTION_SERVICE_STATUS_CHANGED" |
|
|
|
const val EXTRA_SERVICE_STATUS = "service_status" |
|
|
|
const val STATUS_STARTED = "started" |
|
|
|
const val STATUS_STOPPED = "stopped" |
|
|
|
|
|
|
|
// 服务状态监听器 |
|
|
|
private var serviceStatusListener: ServiceStatusListener? = null |
|
|
|
|
|
|
|
// 设置服务状态监听器 |
|
|
|
fun setServiceStatusListener(listener: ServiceStatusListener) { |
|
|
|
serviceStatusListener = listener |
|
|
|
} |
|
|
|
|
|
|
|
// 移除服务状态监听器 |
|
|
|
fun removeServiceStatusListener() { |
|
|
|
serviceStatusListener = null |
|
|
|
} |
|
|
|
|
|
|
|
// 服务状态监听接口 |
|
|
|
interface ServiceStatusListener { |
|
|
|
fun onServiceStarted() |
|
|
|
fun onServiceStopped() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private lateinit var mediaSession: MediaSessionCompat |
|
|
|
private var isRecording = false |
|
|
|
private var isProcessing = false |
|
|
|
private var isActive = false // 合并 isRecording 和 isProcessing 为一个状态 |
|
|
|
private var lastKeyEventTime = 0L |
|
|
|
private var keyEventCount = 0 |
|
|
|
private lateinit var audioManager: AudioManager |
|
|
|
|
|
|
|
// 超时处理 |
|
|
|
private val handler = Handler(Looper.getMainLooper()) |
|
|
|
private var timeoutRunnable: Runnable? = null |
|
|
|
private var lastActivityTime = 0L |
|
|
|
private var isTtsSpeaking = false |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Azure服务 |
|
|
|
private lateinit var azureAsrHelper: AzureAsrHelper |
|
|
|
private lateinit var azureTtsHelper: AzureTtsHelper |
|
|
|
@ -67,77 +104,112 @@ class VoiceInteractionService : Service() { |
|
|
|
|
|
|
|
override fun onCreate() { |
|
|
|
super.onCreate() |
|
|
|
Log.d(TAG, "onCreate") |
|
|
|
Log.d(TAG, "onCreate - 语音交互服务正在创建") |
|
|
|
isRunning.set(true) |
|
|
|
isActive = false // 确保初始状态为 false |
|
|
|
isTtsSpeaking = false // 确保初始状态为 false |
|
|
|
|
|
|
|
// 获取 AudioManager |
|
|
|
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager |
|
|
|
|
|
|
|
// 1. 初始化Azure服务 |
|
|
|
initAzureServices() |
|
|
|
|
|
|
|
// 2. 初始化火山AI服务 |
|
|
|
volcanoAIService = VolcanoAIService(applicationContext) |
|
|
|
// 初始化服务 |
|
|
|
initServices() |
|
|
|
|
|
|
|
// 3. 初始化MediaSession |
|
|
|
// 初始化 MediaSession 和按键监听 |
|
|
|
initMediaSession() |
|
|
|
|
|
|
|
// 4. 启动前台通知(Foreground Service) |
|
|
|
// 启动前台通知(Foreground Service) |
|
|
|
startForegroundServiceWithNotification() |
|
|
|
|
|
|
|
// 5. 注册成为媒体按钮接收器 |
|
|
|
// 注册成为媒体按钮接收器 |
|
|
|
registerMediaButtonReceiver() |
|
|
|
|
|
|
|
// 6. 测试按键事件 |
|
|
|
testKeyEvent() |
|
|
|
// 发送服务已启动的广播 |
|
|
|
sendServiceStatusBroadcast(STATUS_STARTED) |
|
|
|
|
|
|
|
// 通知监听器服务已启动 |
|
|
|
serviceStatusListener?.onServiceStarted() |
|
|
|
|
|
|
|
Log.d(TAG, "VoiceInteractionService 初始化完成,等待蓝牙按键事件...") |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 测试按键事件,模拟一个按键事件以验证处理逻辑 |
|
|
|
* 测试按键事件 |
|
|
|
*/ |
|
|
|
private fun testKeyEvent() { |
|
|
|
Log.d(TAG, "测试按键事件处理") |
|
|
|
Log.d(TAG, "测试按键事件") |
|
|
|
|
|
|
|
// 创建一个模拟的按键事件 |
|
|
|
val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK) |
|
|
|
|
|
|
|
// 创建一个媒体按钮意图 |
|
|
|
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { |
|
|
|
putExtra(Intent.EXTRA_KEY_EVENT, keyEvent) |
|
|
|
component = ComponentName(packageName, VoiceInteractionService::class.java.name) |
|
|
|
} |
|
|
|
|
|
|
|
// MediaButtonReceiver.handleIntent 返回的是 KeyEvent 类型,而不是 Boolean |
|
|
|
// 发送给 MediaSession 处理 |
|
|
|
MediaButtonReceiver.handleIntent(mediaSession, mediaButtonIntent) |
|
|
|
Log.d(TAG, "测试按键事件已发送") |
|
|
|
|
|
|
|
// 直接处理按键事件 |
|
|
|
// // 直接处理按键事件 |
|
|
|
handleKeyEvent(keyEvent) |
|
|
|
|
|
|
|
Log.d(TAG, "测试按键事件已发送") |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 初始化所有服务 |
|
|
|
*/ |
|
|
|
private fun initServices() { |
|
|
|
Log.d(TAG, "初始化所有服务") |
|
|
|
|
|
|
|
// 1. 初始化Azure服务 |
|
|
|
initAzureServices() |
|
|
|
|
|
|
|
// 2. 初始化火山AI服务 |
|
|
|
volcanoAIService = VolcanoAIService(applicationContext) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理按键事件 |
|
|
|
*/ |
|
|
|
private fun handleKeyEvent(keyEvent: KeyEvent) { |
|
|
|
Log.d(TAG, "处理按键事件开始: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}") |
|
|
|
|
|
|
|
if (keyEvent.action == KeyEvent.ACTION_DOWN) { |
|
|
|
when (keyEvent.keyCode) { |
|
|
|
KeyEvent.KEYCODE_MEDIA_PLAY, |
|
|
|
KeyEvent.KEYCODE_MEDIA_PAUSE, |
|
|
|
KeyEvent.KEYCODE_HEADSETHOOK, |
|
|
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { |
|
|
|
Log.d(TAG, "处理按键事件: ${keyEvent.keyCode}") |
|
|
|
Log.d(TAG, "处理媒体按键事件: ${keyEvent.keyCode}") |
|
|
|
|
|
|
|
// 检测双击 |
|
|
|
val currentTime = System.currentTimeMillis() |
|
|
|
if (currentTime - lastKeyEventTime < 500) { // 500毫秒内的按键视为连续按键 |
|
|
|
val timeDiff = currentTime - lastKeyEventTime |
|
|
|
Log.d(TAG, "按键时间差: $timeDiff ms (上次: $lastKeyEventTime, 当前: $currentTime)") |
|
|
|
|
|
|
|
if (timeDiff < 500) { // 500毫秒内的按键视为连续按键 |
|
|
|
keyEventCount++ |
|
|
|
Log.d(TAG, "连续按键计数: $keyEventCount") |
|
|
|
|
|
|
|
if (keyEventCount == 1) { // 这是第二次按键,即双击 |
|
|
|
Log.d(TAG, "检测到双击,开始语音交互") |
|
|
|
handleDoubleClick() |
|
|
|
keyEventCount = 0 |
|
|
|
} |
|
|
|
} else { |
|
|
|
Log.d(TAG, "单击事件,重置计数") |
|
|
|
keyEventCount = 0 |
|
|
|
} |
|
|
|
lastKeyEventTime = currentTime |
|
|
|
} |
|
|
|
else -> { |
|
|
|
Log.d(TAG, "未处理的按键代码: ${keyEvent.keyCode}") |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
Log.d(TAG, "忽略非按下事件: ${keyEvent.action}") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -157,24 +229,30 @@ class VoiceInteractionService : Service() { |
|
|
|
azureAsrHelper = AzureAsrHelper() |
|
|
|
azureTtsHelper = AzureTtsHelper() |
|
|
|
|
|
|
|
// 从应用的SharedPreferences获取Azure配置 |
|
|
|
val sharedPreferences = applicationContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) |
|
|
|
val subscriptionKey = sharedPreferences.getString("flutter.azure_speech_key", "") ?: "" |
|
|
|
val serviceRegion = sharedPreferences.getString("flutter.azure_speech_region", "") ?: "" |
|
|
|
// 从 MainActivity 的静态变量获取 Azure 配置 |
|
|
|
val subscriptionKey = MainActivity.azureSpeechKey |
|
|
|
val serviceRegion = MainActivity.azureSpeechRegion |
|
|
|
|
|
|
|
Log.d(TAG, "Azure配置信息: subscriptionKey=$subscriptionKey, serviceRegion=$serviceRegion") |
|
|
|
Log.d(TAG, "Azure配置信息: subscriptionKey=${if (subscriptionKey.isNotEmpty()) subscriptionKey.take(5) + "..." else "空"}, serviceRegion=${if (serviceRegion.isNotEmpty()) serviceRegion else "空"}") |
|
|
|
|
|
|
|
if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) { |
|
|
|
// 初始化ASR |
|
|
|
azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") |
|
|
|
val asrInitResult = azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") |
|
|
|
Log.d(TAG, "ASR初始化结果: $asrInitResult") |
|
|
|
|
|
|
|
// 初始化TTS |
|
|
|
azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") |
|
|
|
val ttsInitResult = azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") |
|
|
|
Log.d(TAG, "TTS初始化结果: $ttsInitResult") |
|
|
|
|
|
|
|
if (!asrInitResult || !ttsInitResult) { |
|
|
|
Log.e(TAG, "Azure服务初始化失败,语音交互功能可能无法正常工作") |
|
|
|
} |
|
|
|
} else { |
|
|
|
Log.e(TAG, "Azure配置信息不完整,无法初始化Azure服务") |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 初始化 MediaSession 以捕获蓝牙耳机按键 |
|
|
|
*/ |
|
|
|
@ -202,6 +280,8 @@ class VoiceInteractionService : Service() { |
|
|
|
// 监听回调 |
|
|
|
mediaSession.setCallback(object : MediaSessionCompat.Callback() { |
|
|
|
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { |
|
|
|
Log.d(TAG, "MediaSession.onMediaButtonEvent: $mediaButtonEvent") |
|
|
|
|
|
|
|
// 根据 Android 版本使用不同的方式获取 KeyEvent |
|
|
|
val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { |
|
|
|
mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) |
|
|
|
@ -213,7 +293,7 @@ class VoiceInteractionService : Service() { |
|
|
|
Log.d(TAG, "收到媒体按钮事件: $keyEvent") |
|
|
|
|
|
|
|
if (keyEvent != null) { |
|
|
|
Log.d(TAG, "按键代码: ${keyEvent.keyCode}, 动作: ${keyEvent.action}") |
|
|
|
Log.d(TAG, "按键详情: 代码=${keyEvent.keyCode}, 动作=${keyEvent.action}, 时间=${keyEvent.eventTime}") |
|
|
|
|
|
|
|
if (keyEvent.action == KeyEvent.ACTION_DOWN) { |
|
|
|
when (keyEvent.keyCode) { |
|
|
|
@ -225,25 +305,31 @@ class VoiceInteractionService : Service() { |
|
|
|
|
|
|
|
// 检测双击 |
|
|
|
val currentTime = System.currentTimeMillis() |
|
|
|
if (currentTime - lastKeyEventTime < 500) { // 500毫秒内的按键视为连续按键 |
|
|
|
val timeDiff = currentTime - lastKeyEventTime |
|
|
|
Log.d(TAG, "MediaSession 按键时间差: $timeDiff ms") |
|
|
|
|
|
|
|
if (timeDiff < 500) { // 500毫秒内的按键视为连续按键 |
|
|
|
keyEventCount++ |
|
|
|
Log.d(TAG, "MediaSession 连续按键计数: $keyEventCount") |
|
|
|
|
|
|
|
if (keyEventCount == 1) { // 这是第二次按键,即双击 |
|
|
|
Log.d(TAG, "检测到双击,开始语音交互") |
|
|
|
Log.d(TAG, "MediaSession 检测到双击,开始语音交互") |
|
|
|
// 重置处理状态,确保可以开始新的语音交互 |
|
|
|
isActive = false |
|
|
|
handleDoubleClick() |
|
|
|
keyEventCount = 0 |
|
|
|
return true |
|
|
|
} |
|
|
|
} else { |
|
|
|
Log.d(TAG, "MediaSession 单击事件,重置计数") |
|
|
|
keyEventCount = 0 |
|
|
|
} |
|
|
|
lastKeyEventTime = currentTime |
|
|
|
|
|
|
|
// 单击也处理 |
|
|
|
if (keyEventCount == 0) { |
|
|
|
Log.d(TAG, "检测到单击,开始语音交互") |
|
|
|
handleDoubleClick() |
|
|
|
return true |
|
|
|
} |
|
|
|
Log.d(TAG, "MediaSession 处理单击事件") |
|
|
|
handleDoubleClick() |
|
|
|
return true |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -273,15 +359,22 @@ class VoiceInteractionService : Service() { |
|
|
|
* 处理耳机双击事件 |
|
|
|
*/ |
|
|
|
private fun handleDoubleClick() { |
|
|
|
if (isProcessing) { |
|
|
|
Log.d(TAG, "正在处理中,忽略此次点击") |
|
|
|
return |
|
|
|
} |
|
|
|
Log.d(TAG, "handleDoubleClick - isActive: $isActive") |
|
|
|
|
|
|
|
// 无论当前状态如何,都先停止 TTS 播放 |
|
|
|
stopCurrentTTS() |
|
|
|
|
|
|
|
if (!isRecording) { |
|
|
|
// 立即播放提示音,不等待语音识别开始 |
|
|
|
playStartPrompt() |
|
|
|
|
|
|
|
if (!isActive) { |
|
|
|
Log.d(TAG, "开始新的语音交互") |
|
|
|
startVoiceInteraction() |
|
|
|
} else { |
|
|
|
stopVoiceInteraction() |
|
|
|
Log.d(TAG, "已经在活动中,只需更新活动时间并继续") |
|
|
|
// 更新最后活动时间 |
|
|
|
updateLastActivityTime() |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -289,68 +382,239 @@ class VoiceInteractionService : Service() { |
|
|
|
* 开始语音交互 |
|
|
|
*/ |
|
|
|
private fun startVoiceInteraction() { |
|
|
|
if (isRecording || isProcessing) { |
|
|
|
Log.d(TAG, "startVoiceInteraction - 当前状态: isActive=$isActive") |
|
|
|
|
|
|
|
if (isActive) { |
|
|
|
Log.d(TAG, "已经在活动中,忽略此次请求") |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// 检查录音权限 |
|
|
|
if (!checkRecordAudioPermission()) { |
|
|
|
Log.e(TAG, "没有录音权限,无法开始语音交互") |
|
|
|
playErrorPrompt("需要录音权限,请在设置中授予权限") |
|
|
|
// 权限处理由 Flutter 端负责,不再尝试通知 |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
isRecording = true |
|
|
|
isProcessing = true |
|
|
|
isActive = true |
|
|
|
|
|
|
|
// 播放提示音 |
|
|
|
playStartPrompt() |
|
|
|
// 更新最后活动时间 |
|
|
|
updateLastActivityTime() |
|
|
|
|
|
|
|
// 启动超时检查 |
|
|
|
startTimeoutCheck() |
|
|
|
|
|
|
|
Log.d(TAG, "设置状态: isActive=true") |
|
|
|
|
|
|
|
// 不再在这里播放提示音,而是在 handleDoubleClick 中播放 |
|
|
|
|
|
|
|
// 开始录音识别 |
|
|
|
azureAsrHelper.startContinuousRecognition(null, object : AzureAsrHelper.ContinuousRecognizeCallback { |
|
|
|
override fun onRecognizing(recognizing: String) { |
|
|
|
// 实时识别结果,可以记录日志 |
|
|
|
Log.d(TAG, "正在识别: $recognizing") |
|
|
|
} |
|
|
|
|
|
|
|
override fun onResult(result: String) { |
|
|
|
// 最终识别结果 |
|
|
|
Log.d(TAG, "语音识别结果: $result") |
|
|
|
try { |
|
|
|
Log.d(TAG, "开始调用 Azure 语音识别") |
|
|
|
azureAsrHelper.startContinuousRecognition(null, object : AzureAsrHelper.ContinuousRecognizeCallback { |
|
|
|
override fun onRecognizing(recognizing: String) { |
|
|
|
// 实时识别结果,可以记录日志 |
|
|
|
Log.d(TAG, "正在识别: $recognizing") |
|
|
|
|
|
|
|
// 如果检测到用户在讲话,停止当前的 TTS 播放 |
|
|
|
if (recognizing.isNotEmpty()) { |
|
|
|
// 停止 TTS 播放 |
|
|
|
stopCurrentTTS() |
|
|
|
|
|
|
|
// 更新最后活动时间 |
|
|
|
updateLastActivityTime() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 停止录音 |
|
|
|
isRecording = false |
|
|
|
override fun onResult(result: String) { |
|
|
|
// 最终识别结果 |
|
|
|
Log.d(TAG, "语音识别结果: $result") |
|
|
|
|
|
|
|
// 更新最后活动时间 |
|
|
|
|
|
|
|
// 如果有识别结果,则发送到VolcanoAI处理,但不停止录音 |
|
|
|
if (result.isNotEmpty()) { |
|
|
|
Log.d(TAG, "有识别结果,开始处理") |
|
|
|
updateLastActivityTime() |
|
|
|
|
|
|
|
processWithVolcanoAI(result) |
|
|
|
|
|
|
|
// 不设置 isActive = false,保持活动状态 |
|
|
|
} else { |
|
|
|
// Log.d(TAG, "没有识别结果,但继续保持活动状态") |
|
|
|
// 即使没有识别结果,也不停止活动状态 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 如果有识别结果,则发送到VolcanoAI处理 |
|
|
|
if (result.isNotEmpty()) { |
|
|
|
processWithVolcanoAI(result) |
|
|
|
} else { |
|
|
|
isProcessing = false |
|
|
|
override fun onSessionStarted() { |
|
|
|
Log.d(TAG, "语音识别会话开始") |
|
|
|
|
|
|
|
// 更新最后活动时间 |
|
|
|
updateLastActivityTime() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
override fun onSessionStopped() { |
|
|
|
Log.d(TAG, "语音识别会话结束") |
|
|
|
// 确保状态被重置 |
|
|
|
if (isActive) { |
|
|
|
isActive = false |
|
|
|
Log.d(TAG, "会话结束时设置状态: isActive=false") |
|
|
|
} |
|
|
|
|
|
|
|
// 停止超时检查 |
|
|
|
stopTimeoutCheck() |
|
|
|
} |
|
|
|
|
|
|
|
override fun onCanceled(reason: String, errorDetails: String) { |
|
|
|
Log.e(TAG, "语音识别取消: $reason, 详情: $errorDetails") |
|
|
|
isActive = false |
|
|
|
Log.d(TAG, "取消时设置状态: isActive=false") |
|
|
|
|
|
|
|
// 停止超时检查 |
|
|
|
stopTimeoutCheck() |
|
|
|
} |
|
|
|
|
|
|
|
override fun onError(error: String) { |
|
|
|
Log.e(TAG, "语音识别错误: $error") |
|
|
|
isActive = false |
|
|
|
Log.d(TAG, "错误时设置状态: isActive=false") |
|
|
|
|
|
|
|
// 停止超时检查 |
|
|
|
stopTimeoutCheck() |
|
|
|
|
|
|
|
// 播放错误提示音 |
|
|
|
playErrorPrompt("语音识别出错") |
|
|
|
} |
|
|
|
}) |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "启动语音识别异常: ${e.message}", e) |
|
|
|
isActive = false |
|
|
|
Log.d(TAG, "异常时设置状态: isActive=false") |
|
|
|
|
|
|
|
override fun onSessionStarted() { |
|
|
|
Log.d(TAG, "语音识别会话开始") |
|
|
|
} |
|
|
|
// 停止超时检查 |
|
|
|
stopTimeoutCheck() |
|
|
|
|
|
|
|
override fun onSessionStopped() { |
|
|
|
Log.d(TAG, "语音识别会话结束") |
|
|
|
} |
|
|
|
// 播放错误提示音 |
|
|
|
playErrorPrompt("启动语音识别失败") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 更新最后活动时间 |
|
|
|
*/ |
|
|
|
private fun updateLastActivityTime() { |
|
|
|
lastActivityTime = System.currentTimeMillis() |
|
|
|
Log.d(TAG, "更新最后活动时间: $lastActivityTime") |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 启动超时检查 |
|
|
|
*/ |
|
|
|
private fun startTimeoutCheck() { |
|
|
|
// 先停止之前的超时检查 |
|
|
|
stopTimeoutCheck() |
|
|
|
|
|
|
|
// 创建新的超时检查 |
|
|
|
timeoutRunnable = Runnable { |
|
|
|
val currentTime = System.currentTimeMillis() |
|
|
|
val elapsedTime = currentTime - lastActivityTime |
|
|
|
|
|
|
|
override fun onCanceled(reason: String, errorDetails: String) { |
|
|
|
Log.e(TAG, "语音识别取消: $reason, 详情: $errorDetails") |
|
|
|
isRecording = false |
|
|
|
isProcessing = false |
|
|
|
} |
|
|
|
Log.d(TAG, "超时检查 - 已经过去 $elapsedTime ms, 是否活动中: $isActive, 是否正在播放TTS: $isTtsSpeaking") |
|
|
|
|
|
|
|
override fun onError(error: String) { |
|
|
|
Log.e(TAG, "语音识别错误: $error") |
|
|
|
isRecording = false |
|
|
|
isProcessing = false |
|
|
|
if (isActive && !isTtsSpeaking &&elapsedTime >= RECOGNITION_TIMEOUT) { |
|
|
|
// 超过30秒没有活动且没有TTS播放,暂停当前语音识别 |
|
|
|
Log.d(TAG, "超过 ${RECOGNITION_TIMEOUT/1000} 秒没有活动,暂停语音识别") |
|
|
|
|
|
|
|
// 播放错误提示音 |
|
|
|
playErrorPrompt() |
|
|
|
// 播放提示音 |
|
|
|
playErrorPrompt("没有听到您说话,已暂停语音识别") |
|
|
|
|
|
|
|
// 暂停录音,但保持服务活动状态 |
|
|
|
// 这样用户可以通过双击重新激活 |
|
|
|
// azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { |
|
|
|
// override fun onResult(result: String) { |
|
|
|
// // 不处理结果 |
|
|
|
// } |
|
|
|
|
|
|
|
// override fun onRecognizing(recognizing: String) { |
|
|
|
// // 不处理中间结果 |
|
|
|
// } |
|
|
|
|
|
|
|
// override fun onSessionStarted() { |
|
|
|
// // 不处理会话开始事件 |
|
|
|
// } |
|
|
|
|
|
|
|
// override fun onSessionStopped() { |
|
|
|
// Log.d(TAG, "超时暂停:语音识别会话已停止") |
|
|
|
// } |
|
|
|
|
|
|
|
// override fun onCanceled(reason: String, errorDetails: String) { |
|
|
|
// Log.d(TAG, "超时暂停:语音识别已取消: $reason") |
|
|
|
// } |
|
|
|
|
|
|
|
// override fun onError(error: String) { |
|
|
|
// Log.e(TAG, "超时暂停:停止语音识别错误: $error") |
|
|
|
// } |
|
|
|
// }) |
|
|
|
|
|
|
|
// 不设置 isActive = false,保持服务活动状态 |
|
|
|
Log.d(TAG, "保持状态: isActive=true") |
|
|
|
|
|
|
|
// 不停止超时检查,继续监控 |
|
|
|
handler.postDelayed(timeoutRunnable!!, 1000) |
|
|
|
} else if (isActive) { |
|
|
|
// 如果仍在活动中,继续检查 |
|
|
|
handler.postDelayed(timeoutRunnable!!, 1000) // 每秒检查一次 |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
// 开始检查 |
|
|
|
handler.postDelayed(timeoutRunnable!!, 1000) |
|
|
|
Log.d(TAG, "启动超时检查") |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 停止超时检查 |
|
|
|
*/ |
|
|
|
private fun stopTimeoutCheck() { |
|
|
|
timeoutRunnable?.let { |
|
|
|
handler.removeCallbacks(it) |
|
|
|
timeoutRunnable = null |
|
|
|
Log.d(TAG, "停止超时检查") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 停止当前的 TTS 播放 |
|
|
|
*/ |
|
|
|
private fun stopCurrentTTS() { |
|
|
|
try { |
|
|
|
if (isTtsSpeaking) { |
|
|
|
Log.d(TAG, "停止当前 TTS 播放") |
|
|
|
azureTtsHelper.stopSpeaking() |
|
|
|
isTtsSpeaking = false |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "停止 TTS 播放异常: ${e.message}", e) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 检查录音权限 |
|
|
|
*/ |
|
|
|
private fun checkRecordAudioPermission(): Boolean { |
|
|
|
val permission = android.Manifest.permission.RECORD_AUDIO |
|
|
|
val result = applicationContext.checkCallingOrSelfPermission(permission) |
|
|
|
val hasPermission = result == android.content.pm.PackageManager.PERMISSION_GRANTED |
|
|
|
Log.d(TAG, "录音权限检查结果: $hasPermission") |
|
|
|
return hasPermission |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 停止语音交互 |
|
|
|
*/ |
|
|
|
private fun stopVoiceInteraction() { |
|
|
|
if (!isRecording) { |
|
|
|
if (!isActive) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
@ -380,37 +644,59 @@ class VoiceInteractionService : Service() { |
|
|
|
Log.e(TAG, "停止语音识别错误: $error") |
|
|
|
} |
|
|
|
}) |
|
|
|
isRecording = false |
|
|
|
isActive = false |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 播放开始提示音 |
|
|
|
*/ |
|
|
|
private fun playStartPrompt() { |
|
|
|
azureTtsHelper.speakText("我在听", object : AzureTtsHelper.TTSCallback { |
|
|
|
override fun onSuccess(message: String) { |
|
|
|
Log.d(TAG, "提示音播放完成") |
|
|
|
} |
|
|
|
|
|
|
|
override fun onError(error: String) { |
|
|
|
Log.e(TAG, "提示音播放错误: $error") |
|
|
|
} |
|
|
|
}) |
|
|
|
Log.d(TAG, "开始播放提示音: '我在听'") |
|
|
|
try { |
|
|
|
isTtsSpeaking = true |
|
|
|
azureTtsHelper.speakText("我在!", object : AzureTtsHelper.TTSCallback { |
|
|
|
override fun onSuccess(message: String) { |
|
|
|
Log.d(TAG, "提示音播放完成: $message") |
|
|
|
isTtsSpeaking = false |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
override fun onError(error: String) { |
|
|
|
Log.e(TAG, "提示音播放错误: $error") |
|
|
|
isTtsSpeaking = false |
|
|
|
// 即使提示音播放失败,也继续进行语音识别 |
|
|
|
} |
|
|
|
}) |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "播放提示音异常: ${e.message}", e) |
|
|
|
isTtsSpeaking = false |
|
|
|
// 即使提示音播放失败,也继续进行语音识别 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 播放错误提示音 |
|
|
|
*/ |
|
|
|
private fun playErrorPrompt() { |
|
|
|
azureTtsHelper.speakText("抱歉,我没有听清", object : AzureTtsHelper.TTSCallback { |
|
|
|
override fun onSuccess(message: String) { |
|
|
|
Log.d(TAG, "错误提示音播放完成") |
|
|
|
} |
|
|
|
|
|
|
|
override fun onError(error: String) { |
|
|
|
Log.e(TAG, "错误提示音播放错误: $error") |
|
|
|
} |
|
|
|
}) |
|
|
|
private fun playErrorPrompt(errorMessage: String = "抱歉,我没有听清") { |
|
|
|
Log.d(TAG, "开始播放错误提示音: '$errorMessage'") |
|
|
|
try { |
|
|
|
isTtsSpeaking = true |
|
|
|
azureTtsHelper.speakText(errorMessage, object : AzureTtsHelper.TTSCallback { |
|
|
|
override fun onSuccess(message: String) { |
|
|
|
Log.d(TAG, "错误提示音播放完成: $message") |
|
|
|
isTtsSpeaking = false |
|
|
|
} |
|
|
|
|
|
|
|
override fun onError(error: String) { |
|
|
|
Log.e(TAG, "错误提示音播放错误: $error") |
|
|
|
isTtsSpeaking = false |
|
|
|
} |
|
|
|
}) |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "播放错误提示音异常: ${e.message}", e) |
|
|
|
isTtsSpeaking = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
@ -420,6 +706,7 @@ class VoiceInteractionService : Service() { |
|
|
|
// 使用原生VolcanoAIService处理 |
|
|
|
Thread { |
|
|
|
try { |
|
|
|
|
|
|
|
// 创建用户消息 |
|
|
|
val messages = JSONArray().apply { |
|
|
|
put(volcanoAIService.createUserMessage(text)) |
|
|
|
@ -432,10 +719,10 @@ class VoiceInteractionService : Service() { |
|
|
|
speakAIResponse(response) |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "VolcanoAI处理错误: ${e.message}") |
|
|
|
isProcessing = false |
|
|
|
// 不设置 isActive = false,保持活动状态 |
|
|
|
|
|
|
|
// 播放错误提示音 |
|
|
|
playErrorPrompt() |
|
|
|
playErrorPrompt("AI处理出错") |
|
|
|
} |
|
|
|
}.start() |
|
|
|
} |
|
|
|
@ -444,15 +731,27 @@ class VoiceInteractionService : Service() { |
|
|
|
* 播放AI回复 |
|
|
|
*/ |
|
|
|
private fun speakAIResponse(text: String) { |
|
|
|
Log.d(TAG, "开始播放AI回复: '${text.take(20)}...'") |
|
|
|
|
|
|
|
isTtsSpeaking = true |
|
|
|
azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback { |
|
|
|
override fun onSuccess(message: String) { |
|
|
|
Log.d(TAG, "AI回复播放完成") |
|
|
|
isProcessing = false |
|
|
|
isTtsSpeaking = false |
|
|
|
|
|
|
|
// 更新最后活动时间 |
|
|
|
updateLastActivityTime() |
|
|
|
|
|
|
|
// 不设置 isActive = false,保持活动状态 |
|
|
|
// 继续保持对话状态,等待用户下一次输入 |
|
|
|
Log.d(TAG, "继续保持对话状态,等待用户下一次输入") |
|
|
|
} |
|
|
|
|
|
|
|
override fun onError(error: String) { |
|
|
|
Log.e(TAG, "AI回复播放错误: $error") |
|
|
|
isProcessing = false |
|
|
|
isTtsSpeaking = false |
|
|
|
// 即使出错,也不结束活动状态,继续保持对话 |
|
|
|
Log.d(TAG, "AI回复播放出错,但继续保持对话状态") |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
@ -461,6 +760,14 @@ class VoiceInteractionService : Service() { |
|
|
|
* 启动一个"前台通知",保证此Service在后台不会被系统回收 |
|
|
|
*/ |
|
|
|
private fun startForegroundServiceWithNotification() { |
|
|
|
val notification = createNotification() |
|
|
|
startForeground(NOTIFICATION_ID, notification) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 创建通知 |
|
|
|
*/ |
|
|
|
private fun createNotification(): Notification { |
|
|
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager |
|
|
|
|
|
|
|
// Android 8.0+ 需要创建通知渠道 |
|
|
|
@ -484,27 +791,25 @@ class VoiceInteractionService : Service() { |
|
|
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT |
|
|
|
) |
|
|
|
|
|
|
|
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID) |
|
|
|
return NotificationCompat.Builder(this, CHANNEL_ID) |
|
|
|
.setContentTitle("语音助手") |
|
|
|
.setContentText("双击蓝牙耳机按键即可开始语音交互") |
|
|
|
.setSmallIcon(R.mipmap.ic_launcher) |
|
|
|
.setContentIntent(pendingIntent) |
|
|
|
.build() |
|
|
|
|
|
|
|
startForeground(NOTIFICATION_ID, notification) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 当 Service 被启动时调用 |
|
|
|
*/ |
|
|
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { |
|
|
|
Log.d(TAG, "onStartCommand: $intent") |
|
|
|
Log.d(TAG, "onStartCommand: $intent, extras: ${intent?.extras}") |
|
|
|
|
|
|
|
// 处理媒体按键 Intent |
|
|
|
if (intent != null) { |
|
|
|
// 如果是媒体按钮事件,尝试手动处理 |
|
|
|
if (Intent.ACTION_MEDIA_BUTTON == intent.action) { |
|
|
|
Log.d(TAG, "收到媒体按钮事件: $intent") |
|
|
|
Log.d(TAG, "收到媒体按钮事件 Intent: $intent, extras: ${intent.extras}") |
|
|
|
|
|
|
|
// 根据 Android 版本使用不同的方式获取 KeyEvent |
|
|
|
val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { |
|
|
|
@ -514,14 +819,23 @@ class VoiceInteractionService : Service() { |
|
|
|
intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent |
|
|
|
} |
|
|
|
|
|
|
|
Log.d(TAG, "手动处理媒体按钮事件: $keyEvent") |
|
|
|
Log.d(TAG, "从 Intent 中提取的按键事件: $keyEvent") |
|
|
|
if (keyEvent != null) { |
|
|
|
Log.d(TAG, "准备手动处理按键事件: 代码=${keyEvent.keyCode}, 动作=${keyEvent.action}") |
|
|
|
handleKeyEvent(keyEvent) |
|
|
|
} else { |
|
|
|
// 即使没有 KeyEvent,也尝试触发语音交互 |
|
|
|
Log.d(TAG, "Intent 中没有按键事件,但仍然尝试触发语音交互") |
|
|
|
// 重置处理状态,确保可以开始新的语音交互 |
|
|
|
isActive = false |
|
|
|
handleDoubleClick() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 无论如何都要尝试让 MediaSession 处理 |
|
|
|
MediaButtonReceiver.handleIntent(mediaSession, intent) |
|
|
|
Log.d(TAG, "将 Intent 传递给 MediaSession 处理") |
|
|
|
val result = MediaButtonReceiver.handleIntent(mediaSession, intent) |
|
|
|
Log.d(TAG, "MediaSession 处理结果: $result") |
|
|
|
} |
|
|
|
|
|
|
|
return START_STICKY |
|
|
|
@ -531,9 +845,12 @@ class VoiceInteractionService : Service() { |
|
|
|
override fun onBind(intent: Intent?): IBinder? = null |
|
|
|
|
|
|
|
override fun onDestroy() { |
|
|
|
Log.d(TAG, "onDestroy") |
|
|
|
Log.d(TAG, "onDestroy - 语音交互服务正在销毁") |
|
|
|
super.onDestroy() |
|
|
|
|
|
|
|
// 停止超时检查 |
|
|
|
stopTimeoutCheck() |
|
|
|
|
|
|
|
// 取消注册媒体按钮接收器 |
|
|
|
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) |
|
|
|
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver) |
|
|
|
@ -547,5 +864,24 @@ class VoiceInteractionService : Service() { |
|
|
|
azureTtsHelper.dispose() |
|
|
|
|
|
|
|
isRunning.set(false) |
|
|
|
|
|
|
|
// 发送服务已停止的广播 |
|
|
|
sendServiceStatusBroadcast(STATUS_STOPPED) |
|
|
|
|
|
|
|
// 通知监听器服务已停止 |
|
|
|
serviceStatusListener?.onServiceStopped() |
|
|
|
|
|
|
|
Log.d(TAG, "语音交互服务已完全销毁") |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 发送服务状态变化广播 |
|
|
|
*/ |
|
|
|
private fun sendServiceStatusBroadcast(status: String) { |
|
|
|
val intent = Intent(ACTION_SERVICE_STATUS_CHANGED).apply { |
|
|
|
putExtra(EXTRA_SERVICE_STATUS, status) |
|
|
|
} |
|
|
|
sendBroadcast(intent) |
|
|
|
Log.d(TAG, "已发送服务状态广播: $status") |
|
|
|
} |
|
|
|
} |