feat(speech): 添加语音识别和合成服务的自动重连机制

- 集成网络状态监听功能,网络恢复时自动重连语音服务
- 实现 ASR 和 TTS 服务的断线重连逻辑,提升连接稳定性
- 添加异常处理和重试机制,防止连接失败导致的服务中断
- 优化网络检测逻辑,统一使用 ConnectivityManager 管理网络状态
- 添加定时重连任务,支持延迟重连和立即重连两种模式
- 完善资源清理机制,确保关闭时正确释放所有连接和回调
这个提交包含在:
徐勤民 2026-04-20 10:52:46 +08:00
父节点 ec83fe0c2d
当前提交 edcd01d146

查看文件

@ -7,11 +7,13 @@ import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import com.blankj.utilcode.util.Utils
import com.nova.brain.glass.BuildConfig
import com.nova.brain.glass.R
import com.nova.brain.glass.model.RecognizeAction
@ -55,10 +57,15 @@ object AsrHelper : OfflineCmdListener {
private var isMicRunning = false
private var isTtsConnected = false
private var isAsrConnecting = false
private var isTtsConnecting = false
private var pendingStartMic = false
private var isClosed = false
private var connectivityManager: ConnectivityManager? = null
private var networkCallbackRegistered = false
private const val WAKE_RESPONSE = "在呢,您请说"
private const val LISTENING_TIMEOUT_MS = 30_000L
private const val RECONNECT_DELAY_MS = 3_000L
private val mainHandler = Handler(Looper.getMainLooper())
private var listeningDialog: AlertDialog? = null
@ -80,6 +87,27 @@ object AsrHelper : OfflineCmdListener {
mainHandler.postDelayed(this, 1000L)
}
}
private val asrReconnectRunnable = Runnable {
if (shouldReconnectAsr()) {
asrConnect()
}
}
private val ttsReconnectRunnable = Runnable {
if (shouldReconnectTts()) {
ttsConnect()
}
}
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Log.d(TAG, "Network available, reconnect speech channels if needed")
reconnectAllIfNeeded(immediate = true)
dismissNoNetworkDialog()
}
override fun onLost(network: Network) {
Log.d(TAG, "Network lost")
}
}
// 拼接每次识别会话中的中间结果
private var currentPartial = ""
@ -103,6 +131,7 @@ object AsrHelper : OfflineCmdListener {
var onDirectChat: ((text: String) -> Unit)? = null
fun init() {
isClosed = false
val cfg = OnlineSpeechSdkConfig(
domain = DOMAIN,
ak = AK,
@ -129,20 +158,42 @@ object AsrHelper : OfflineCmdListener {
// 注册离线关键词 Nova Nova,GlassSdk 触发后启动 ASR
OfflineCmdServiceHelper.registerAsrWakeWord()
OfflineCmdServiceHelper.addOnLineListener(this)
registerNetworkCallback()
// 自动建立 ASR / TTS 连接
asrConnect()
tts?.connect()
ttsConnect()
Log.d(TAG, "AsrHelper init done")
}
private fun asrConnect() {
if (isConnected || isAsrConnecting) return
if (!shouldReconnectAsr()) return
isAsrConnecting = true
asr?.connect()
runCatching { asr?.connect() }
.onSuccess {
Log.d(TAG, "ASR connect() called")
}
.onFailure {
isAsrConnecting = false
Log.e(TAG, "ASR connect failed: ${it.message}")
scheduleAsrReconnect()
}
}
private fun ttsConnect() {
if (!shouldReconnectTts()) return
isTtsConnecting = true
runCatching { tts?.connect() }
.onSuccess {
Log.d(TAG, "TTS connect() called")
}
.onFailure {
isTtsConnecting = false
Log.e(TAG, "TTS connect failed: ${it.message}")
scheduleTtsReconnect()
}
}
private fun asrStartMic() {
if (!isConnected) {
@ -173,6 +224,64 @@ object AsrHelper : OfflineCmdListener {
mainHandler.removeCallbacks(listeningTimeoutRunnable)
}
private fun scheduleAsrReconnect(delayMs: Long = RECONNECT_DELAY_MS) {
if (!shouldReconnectAsr()) return
mainHandler.removeCallbacks(asrReconnectRunnable)
mainHandler.postDelayed(asrReconnectRunnable, delayMs)
Log.d(TAG, "ASR reconnect scheduled in ${delayMs}ms")
}
private fun scheduleTtsReconnect(delayMs: Long = RECONNECT_DELAY_MS) {
if (!shouldReconnectTts()) return
mainHandler.removeCallbacks(ttsReconnectRunnable)
mainHandler.postDelayed(ttsReconnectRunnable, delayMs)
Log.d(TAG, "TTS reconnect scheduled in ${delayMs}ms")
}
private fun reconnectAllIfNeeded(immediate: Boolean = false) {
if (!isNetworkAvailable()) return
val delayMs = if (immediate) 0L else RECONNECT_DELAY_MS
if (!isConnected) {
scheduleAsrReconnect(delayMs)
}
if (!isTtsConnected) {
scheduleTtsReconnect(delayMs)
}
}
private fun shouldReconnectAsr(): Boolean {
return !isClosed && !isConnected && !isAsrConnecting && isNetworkAvailable()
}
private fun shouldReconnectTts(): Boolean {
return !isClosed && !isTtsConnected && !isTtsConnecting && isNetworkAvailable()
}
private fun getContext(): Context = Utils.getApp()
private fun registerNetworkCallback() {
if (networkCallbackRegistered) return
val manager = getContext().getSystemService(ConnectivityManager::class.java) ?: return
connectivityManager = manager
runCatching {
manager.registerDefaultNetworkCallback(networkCallback)
networkCallbackRegistered = true
}.onFailure {
Log.e(TAG, "register network callback failed: ${it.message}")
}
}
private fun unregisterNetworkCallback() {
if (!networkCallbackRegistered) return
runCatching {
connectivityManager?.unregisterNetworkCallback(networkCallback)
}.onFailure {
Log.w(TAG, "unregister network callback failed: ${it.message}")
}
networkCallbackRegistered = false
connectivityManager = null
}
private fun showListeningDialog() {
mainHandler.post {
listeningDialog?.dismiss()
@ -254,12 +363,12 @@ object AsrHelper : OfflineCmdListener {
}
private fun isNetworkAvailable(): Boolean {
val activity = runCatching { AppManager.getInstance().getActivity() }.getOrNull()
val context: Context = activity ?: return false
val connectivityManager =
context.getSystemService(ConnectivityManager::class.java) ?: return false
val network = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
val manager = connectivityManager
?: getContext().getSystemService(ConnectivityManager::class.java)
?: return false
connectivityManager = manager
val network = manager.activeNetwork ?: return false
val capabilities = manager.getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
@ -268,6 +377,7 @@ object AsrHelper : OfflineCmdListener {
override fun onOpen() {
isConnected = true
isAsrConnecting = false
mainHandler.removeCallbacks(asrReconnectRunnable)
Log.d(TAG, "ASR websocket open")
if (pendingStartMic) {
asrStartMic()
@ -329,6 +439,7 @@ object AsrHelper : OfflineCmdListener {
isAsrConnecting = false
isMicRunning = false
dismissListeningDialog()
scheduleAsrReconnect()
}
override fun onClosed(code: Int, reason: String) {
@ -337,6 +448,7 @@ object AsrHelper : OfflineCmdListener {
isMicRunning = false
dismissListeningDialog()
Log.d(TAG, "ASR closed code=$code reason=$reason")
scheduleAsrReconnect()
}
})
}
@ -345,6 +457,8 @@ object AsrHelper : OfflineCmdListener {
ttsClient.setListener(object : TtsClient.Listener {
override fun onOpen() {
isTtsConnected = true
isTtsConnecting = false
mainHandler.removeCallbacks(ttsReconnectRunnable)
Log.d(TAG, "TTS websocket open")
}
@ -354,13 +468,18 @@ object AsrHelper : OfflineCmdListener {
}
override fun onError(code: Int, message: String) {
isTtsConnected = false
isTtsConnecting = false
Log.e(TAG, "TTS error code=$code msg=$message, fallback to mic")
scheduleTtsReconnect()
asrStartMic()
}
override fun onClosed(code: Int, reason: String) {
isTtsConnected = false
isTtsConnecting = false
Log.d(TAG, "TTS closed code=$code reason=$reason")
scheduleTtsReconnect()
}
})
}
@ -415,14 +534,18 @@ object AsrHelper : OfflineCmdListener {
}
fun close() {
isClosed = true
OfflineCmdServiceHelper.removeOnLineListener(this)
mainHandler.removeCallbacks(networkCheckRunnable)
mainHandler.removeCallbacks(asrReconnectRunnable)
mainHandler.removeCallbacks(ttsReconnectRunnable)
dismissNoNetworkDialog()
if (isMicRunning) {
runCatching { asr?.stopAsrWithMic() }
isMicRunning = false
}
dismissListeningDialog()
unregisterNetworkCallback()
asr?.close()
tts?.close()
sdk?.close()
@ -430,7 +553,9 @@ object AsrHelper : OfflineCmdListener {
tts = null
sdk = null
isConnected = false
isAsrConnecting = false
isTtsConnected = false
isTtsConnecting = false
Log.d(TAG, "AsrHelper closed")
}
}