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