From c0643d41dfbb696ba4d0cf4ade43340cd572523b Mon Sep 17 00:00:00 2001 From: dongliang Date: Fri, 8 May 2026 15:12:19 +0930 Subject: [PATCH] =?UTF-8?q?revert:=20=E5=9B=9E=E6=BB=9A=20TTS=20=E8=AF=AD?= =?UTF-8?q?=E9=9F=B3=E6=92=AD=E6=8A=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Edge TTS (speech.platform.bing.com) 在手表网络环境下无法连接, 回滚所有 TTS 相关改动,恢复到 fec1e80 的状态。 后续待确定可用的 TTS 方案后再重新实现。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../device/sensor/FiseVibrationController.kt | 9 +- .../watch/service/manager/EdgeTtsManager.kt | 363 ------------------ .../com/xiaoqu/watch/ui/home/HomeFragment.kt | 25 +- .../watch/ui/task/TaskDetailFragment.kt | 100 ----- .../xiaoqu/watch/ui/task/TaskListFragment.kt | 102 ----- app/src/main/res/drawable/bg_tts_button.xml | 6 - .../res/drawable/bg_tts_button_active.xml | 6 - app/src/main/res/drawable/ic_speaker.xml | 19 - app/src/main/res/drawable/ic_speaker_stop.xml | 22 -- .../main/res/layout/fragment_task_detail.xml | 12 - .../main/res/layout/fragment_task_list.xml | 13 - 11 files changed, 5 insertions(+), 672 deletions(-) delete mode 100644 app/src/main/java/com/xiaoqu/watch/service/manager/EdgeTtsManager.kt delete mode 100644 app/src/main/res/drawable/bg_tts_button.xml delete mode 100644 app/src/main/res/drawable/bg_tts_button_active.xml delete mode 100644 app/src/main/res/drawable/ic_speaker.xml delete mode 100644 app/src/main/res/drawable/ic_speaker_stop.xml diff --git a/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseVibrationController.kt b/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseVibrationController.kt index 4edd0ea..4d3a324 100644 --- a/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseVibrationController.kt +++ b/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseVibrationController.kt @@ -24,8 +24,7 @@ import javax.inject.Singleton @Singleton class FiseVibrationController @Inject constructor( @ApplicationContext private val context: Context, - private val configManager: VibrationConfigManager, - private val edgeTtsManager: com.xiaoqu.watch.service.manager.EdgeTtsManager + private val configManager: VibrationConfigManager ) : VibrationController { /** 系统振动器 */ @@ -99,11 +98,9 @@ class FiseVibrationController @Inject constructor( } } - // 音频:系统级开关 + 用户级开关 + 有音频文件 + TTS 未在播放 - if (voiceOk && !edgeTtsManager.isPlaying) { + // 音频:系统级开关 + 用户级开关 + 有音频文件 + if (voiceOk) { playAudio(pattern.audioResId, configManager.voiceVolume) - } else if (voiceOk && edgeTtsManager.isPlaying) { - Timber.d("振动: TTS 播放中,跳过提示音") } } diff --git a/app/src/main/java/com/xiaoqu/watch/service/manager/EdgeTtsManager.kt b/app/src/main/java/com/xiaoqu/watch/service/manager/EdgeTtsManager.kt deleted file mode 100644 index 23e9599..0000000 --- a/app/src/main/java/com/xiaoqu/watch/service/manager/EdgeTtsManager.kt +++ /dev/null @@ -1,363 +0,0 @@ -package com.xiaoqu.watch.service.manager - -import android.content.Context -import android.media.MediaPlayer -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.* -import okhttp3.* -import okhttp3.ConnectionSpec -import okhttp3.TlsVersion -import okio.ByteString -import timber.log.Timber -import java.io.ByteArrayOutputStream -import java.io.File -import java.security.MessageDigest -import java.text.SimpleDateFormat -import java.util.* -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Edge TTS 语音合成管理器 - * 使用微软 Edge 浏览器的免费 TTS 服务,支持高质量中文语音合成。 - * 通过 WebSocket 连接,发送文本,接收 MP3 音频并播放。 - */ -@Singleton -class EdgeTtsManager @Inject constructor( - @ApplicationContext private val context: Context -) { - - /** - * TTS 专用 OkHttpClient(不带业务拦截器) - * 业务拦截器(SignatureInterceptor、UnbindInterceptor)会干扰 WebSocket 连接 - */ - private val ttsClient: OkHttpClient = run { - // Android 8.1 TLS 兼容:强制启用 TLS 1.2 + 现代密码套件 - val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .tlsVersions(TlsVersion.TLS_1_2) - .build() - val compatSpec = ConnectionSpec.Builder(ConnectionSpec.COMPATIBLE_TLS) - .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1) - .build() - OkHttpClient.Builder() - .connectionSpecs(listOf(spec, compatSpec, ConnectionSpec.CLEARTEXT)) - .connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS) - .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS) - .build() - } - - companion object { - private const val TAG = "EdgeTTS" - - /** WebSocket 基础地址 */ - private const val WSS_BASE = "wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1" - - /** 固定的信任客户端 Token */ - private const val TRUSTED_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4" - - /** GEC 版本号 */ - private const val GEC_VERSION = "1-143.0.3650.75" - - /** Windows 纪元偏移(Unix epoch → Windows file time epoch) */ - private const val WINDOWS_EPOCH_OFFSET = 11644473600L - - /** 默认中文女声(晓晓,微软神经网络语音,音质最好) */ - const val VOICE_XIAOXIAO = "zh-CN-XiaoxiaoNeural" - - /** 中文男声(云希) */ - const val VOICE_YUNXI = "zh-CN-YunxiNeural" - - /** 音频输出格式:24kHz 48kbps MP3,体积小质量够用 */ - private const val OUTPUT_FORMAT = "audio-24khz-48kbitrate-mono-mp3" - - /** 缓存目录名 */ - private const val CACHE_DIR = "tts_cache" - - /** 缓存最大条数 */ - private const val MAX_CACHE_SIZE = 50 - } - - /** 当前 MediaPlayer */ - private var mediaPlayer: MediaPlayer? = null - - /** 是否正在播放 */ - var isPlaying: Boolean = false - private set - - /** 时钟偏移(秒),用于 GEC Token 生成 */ - private var clockSkewSeconds: Long = 0 - - /** 播放完成回调 */ - var onComplete: (() -> Unit)? = null - - /** - * 合成并播放语音 - * @param text 要朗读的文本 - * @param voice 语音类型,默认晓晓 - * @param onError 错误回调 - */ - fun speak(text: String, voice: String = VOICE_XIAOXIAO, onError: ((String) -> Unit)? = null) { - if (text.isBlank()) return - - // 停止当前播放 - stop() - - CoroutineScope(Dispatchers.IO).launch { - try { - // 检查缓存 - val cacheFile = getCacheFile(text, voice) - if (cacheFile.exists()) { - Timber.d("$TAG: 命中缓存 ${cacheFile.name}") - playAudio(cacheFile) - return@launch - } - - // 调用 Edge TTS API - val audioData = synthesize(text, voice) - if (audioData != null && audioData.isNotEmpty()) { - // 保存到缓存 - saveToCacheDir(cacheFile, audioData) - playAudio(cacheFile) - } else { - Timber.w("$TAG: 合成返回空数据") - withContext(Dispatchers.Main) { onError?.invoke("语音合成失败") } - } - } catch (e: Exception) { - Timber.e(e, "$TAG: 语音合成异常") - withContext(Dispatchers.Main) { onError?.invoke("语音合成异常: ${e.message}") } - } - } - } - - /** 停止播放 */ - fun stop() { - try { - mediaPlayer?.apply { - if (isPlaying) stop() - release() - } - } catch (_: Exception) { - } - mediaPlayer = null - isPlaying = false - } - - /** - * 通过 WebSocket 调用 Edge TTS 合成语音 - * @return MP3 音频字节数组,失败返回 null - */ - private suspend fun synthesize(text: String, voice: String): ByteArray? { - return suspendCancellableCoroutine { continuation -> - val connectionId = UUID.randomUUID().toString().replace("-", "") - val requestId = UUID.randomUUID().toString().replace("-", "") - val gecToken = generateGecToken() - - val url = "$WSS_BASE?TrustedClientToken=$TRUSTED_TOKEN" + - "&ConnectionId=$connectionId" + - "&Sec-MS-GEC=$gecToken" + - "&Sec-MS-GEC-Version=$GEC_VERSION" - - val request = Request.Builder() - .url(url) - .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0") - .header("Origin", "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold") - .build() - - val audioBuffer = ByteArrayOutputStream() - var resumed = false - - val ws = ttsClient.newWebSocket(request, object : WebSocketListener() { - override fun onOpen(webSocket: WebSocket, response: Response) { - Timber.d("$TAG: WebSocket 已连接") - - // 1. 发送 speech.config - val configMsg = buildConfigMessage() - webSocket.send(configMsg) - - // 2. 发送 SSML 合成请求 - val ssmlMsg = buildSsmlMessage(requestId, text, voice) - webSocket.send(ssmlMsg) - } - - override fun onMessage(webSocket: WebSocket, text: String) { - // 文本帧:解析 Path - if (text.contains("Path:turn.end")) { - // 合成完成 - Timber.d("$TAG: 合成完成,音频大小 ${audioBuffer.size()} 字节") - webSocket.close(1000, "done") - if (!resumed) { - resumed = true - continuation.resumeWith(Result.success(audioBuffer.toByteArray())) - } - } - } - - override fun onMessage(webSocket: WebSocket, bytes: ByteString) { - // 二进制帧:提取音频数据 - val data = bytes.toByteArray() - val audioPayload = parseBinaryFrame(data) - if (audioPayload != null) { - audioBuffer.write(audioPayload) - } - } - - override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { - Timber.e(t, "$TAG: WebSocket 连接失败, response=${response?.code}, url=$url") - // 尝试从错误响应中修正时钟偏移 - response?.header("Date")?.let { adjustClockSkew(it) } - if (!resumed) { - resumed = true - continuation.resumeWith(Result.success(null)) - } - } - - override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { - if (!resumed) { - resumed = true - continuation.resumeWith(Result.success(audioBuffer.toByteArray())) - } - } - }) - - continuation.invokeOnCancellation { - ws.cancel() - } - } - } - - /** 构建 speech.config 消息 */ - private fun buildConfigMessage(): String { - val timestamp = formatTimestamp() - return "X-Timestamp:$timestamp\r\n" + - "Content-Type:application/json; charset=utf-8\r\n" + - "Path:speech.config\r\n" + - "\r\n" + - """{"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":"false","wordBoundaryEnabled":"false"},"outputFormat":"$OUTPUT_FORMAT"}}}}""" - } - - /** 构建 SSML 合成请求消息 */ - private fun buildSsmlMessage(requestId: String, text: String, voice: String): String { - val timestamp = formatTimestamp() - val escapedText = text - .replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'") - - return "X-RequestId:$requestId\r\n" + - "Content-Type:application/ssml+xml\r\n" + - "X-Timestamp:${timestamp}Z\r\n" + - "Path:ssml\r\n" + - "\r\n" + - "" + - "" + - "$escapedText" + - "" - } - - /** - * 解析二进制帧,提取音频数据 - * 格式:[2字节头部长度][头部内容][音频数据] - */ - private fun parseBinaryFrame(data: ByteArray): ByteArray? { - if (data.size < 2) return null - val headerLength = ((data[0].toInt() and 0xFF) shl 8) or (data[1].toInt() and 0xFF) - val audioStart = 2 + headerLength - if (audioStart >= data.size) return null - - // 验证是音频帧 - val headerStr = String(data, 2, headerLength, Charsets.US_ASCII) - if (!headerStr.contains("Path:audio")) return null - - return data.copyOfRange(audioStart, data.size) - } - - /** 生成 Sec-MS-GEC Token(基于时间的 SHA256 哈希) */ - private fun generateGecToken(): String { - var ticks = (System.currentTimeMillis() / 1000.0) + clockSkewSeconds - ticks += WINDOWS_EPOCH_OFFSET - ticks -= ticks % 300 // 对齐到 5 分钟 - ticks *= 10_000_000 // 转换为 100 纳秒间隔 - - val strToHash = "${ticks.toLong()}$TRUSTED_TOKEN" - val digest = MessageDigest.getInstance("SHA-256") - .digest(strToHash.toByteArray(Charsets.US_ASCII)) - return digest.joinToString("") { "%02X".format(it) } - } - - /** 格式化时间戳 */ - private fun formatTimestamp(): String { - val sdf = SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss 'GMT+0000 (Coordinated Universal Time)'", Locale.US) - sdf.timeZone = TimeZone.getTimeZone("UTC") - return sdf.format(Date()) - } - - /** 从服务器响应修正时钟偏移 */ - private fun adjustClockSkew(serverDateHeader: String) { - try { - val sdf = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US) - val serverTime = sdf.parse(serverDateHeader)?.time ?: return - clockSkewSeconds = (serverTime - System.currentTimeMillis()) / 1000 - Timber.d("$TAG: 时钟偏移修正为 ${clockSkewSeconds}s") - } catch (_: Exception) { - } - } - - /** 播放音频文件 */ - private suspend fun playAudio(file: File) { - withContext(Dispatchers.Main) { - try { - stop() - val player = MediaPlayer() - player.setDataSource(file.absolutePath) - player.setOnCompletionListener { - isPlaying = false - onComplete?.invoke() - Timber.d("$TAG: 播放完成") - } - player.setOnErrorListener { _, what, extra -> - Timber.e("$TAG: 播放错误 what=$what extra=$extra") - isPlaying = false - true - } - player.prepare() - player.start() - mediaPlayer = player - isPlaying = true - Timber.d("$TAG: 开始播放") - } catch (e: Exception) { - Timber.e(e, "$TAG: 播放异常") - isPlaying = false - } - } - } - - // ===== 缓存管理 ===== - - /** 获取缓存文件路径(基于文本+语音的 MD5) */ - private fun getCacheFile(text: String, voice: String): File { - val cacheDir = File(context.cacheDir, CACHE_DIR).also { it.mkdirs() } - val key = MessageDigest.getInstance("MD5") - .digest("$voice:$text".toByteArray()) - .joinToString("") { "%02x".format(it) } - return File(cacheDir, "$key.mp3") - } - - /** 保存到缓存目录,超过上限时清理最旧的 */ - private fun saveToCacheDir(file: File, data: ByteArray) { - file.writeBytes(data) - // 清理超出上限的旧缓存 - val cacheDir = file.parentFile ?: return - val files = cacheDir.listFiles()?.sortedBy { it.lastModified() } ?: return - if (files.size > MAX_CACHE_SIZE) { - files.take(files.size - MAX_CACHE_SIZE).forEach { it.delete() } - } - } - - /** 清除所有缓存 */ - fun clearCache() { - File(context.cacheDir, CACHE_DIR).deleteRecursively() - Timber.d("$TAG: 缓存已清除") - } -} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt index 2fb2e4d..6fed5ca 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt @@ -28,7 +28,6 @@ import com.xiaoqu.watch.ui.punch.PunchResult import com.xiaoqu.watch.ui.punch.PunchViewModel import com.xiaoqu.watch.ui.widget.StatusBarView import com.xiaoqu.watch.util.DateUtil -import com.xiaoqu.watch.service.manager.EdgeTtsManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -52,7 +51,6 @@ class HomeFragment : BaseFragment() { @Inject lateinit var bluetoothScanManager: com.xiaoqu.watch.service.manager.BluetoothScanManager @Inject lateinit var notificationManager: com.xiaoqu.watch.service.manager.NotificationManager @Inject lateinit var vibrationConfigManager: com.xiaoqu.watch.device.sensor.VibrationConfigManager - @Inject lateinit var edgeTtsManager: EdgeTtsManager /** 考勤打卡 ViewModel */ private val punchViewModel: PunchViewModel by viewModels() @@ -85,8 +83,6 @@ class HomeFragment : BaseFragment() { private var debugTapCount = 0 private var lastTapTime = 0L - // ===== TTS 语音测试 ===== - override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding { return FragmentHomeBinding.inflate(inflater, container, false) } @@ -183,8 +179,6 @@ class HomeFragment : BaseFragment() { it.onBackKeyPressed = null it.notificationBanner.onClick = null } - // 停止 TTS 播放 - edgeTtsManager.stop() } // ===== 打卡面板 ===== @@ -554,26 +548,11 @@ class HomeFragment : BaseFragment() { if (debugTapCount >= 6) { debugTapCount = 0 - Timber.d("TTS 语音测试开始") - testTts() + Timber.d("调试模式已开启") + Toast.makeText(requireContext(), "调试模式已开启", Toast.LENGTH_SHORT).show() } } - /** - * Edge TTS 语音测试:通过微软 Edge TTS 合成中文语音 - * 测试内容:调用 Edge TTS API → 接收 MP3 → 播放 - * 结果通过 Logcat 和 Toast 反馈 - */ - private fun testTts() { - Toast.makeText(requireContext(), "Edge TTS 测试中...", Toast.LENGTH_SHORT).show() - edgeTtsManager.speak( - text = "您有3条新任务待处理,请及时查看", - onError = { msg -> - Toast.makeText(requireContext(), "TTS 失败: $msg", Toast.LENGTH_LONG).show() - } - ) - } - // ===== 事件监听 ===== /** 监听 MQTT 和系统状态事件 */ diff --git a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt index 7d3d358..7075b60 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt @@ -13,7 +13,6 @@ import com.xiaoqu.watch.databinding.FragmentTaskDetailBinding import com.xiaoqu.watch.network.ApiResult import com.xiaoqu.watch.network.api.TaskApi import com.xiaoqu.watch.network.safeApiCall -import com.xiaoqu.watch.service.manager.EdgeTtsManager import com.xiaoqu.watch.service.manager.NfcTaskManager import com.xiaoqu.watch.ui.common.BaseFragment import com.xiaoqu.watch.ui.widget.QuTipDialog @@ -31,7 +30,6 @@ class TaskDetailFragment : BaseFragment() { @Inject lateinit var taskApi: TaskApi @Inject lateinit var nfcTaskManager: NfcTaskManager - @Inject lateinit var edgeTtsManager: EdgeTtsManager /** 当前任务数据 */ private var taskDetail: TaskDetail? = null @@ -55,9 +53,6 @@ class TaskDetailFragment : BaseFragment() { findNavController().popBackStack() } - // TTS 播放按钮 - binding.btnTts.setOnClickListener { toggleTts() } - // 从导航参数获取任务 ID val taskId = arguments?.getLong("taskId", 0) ?: 0 if (taskId > 0) { @@ -289,99 +284,4 @@ class TaskDetailFragment : BaseFragment() { } } } - - // ===== TTS 语音播报 ===== - - /** 切换播放/停止 */ - private fun toggleTts() { - if (edgeTtsManager.isPlaying) { - stopTts() - } else { - startTts() - } - } - - /** 开始播报当前任务内容 */ - private fun startTts() { - val detail = taskDetail ?: return - - // 拼接播报文本:有值的字段才读,空字段跳过 - val parts = mutableListOf() - if (detail.name.isNotBlank()) parts.add("任务:${detail.displayName}") - if (detail.positionText.isNotBlank()) parts.add("地点:${detail.positionText}") - if (detail.sendTime.isNotBlank()) parts.add("派单时间:${formatTimeForTts(detail.sendTime)}") - when { - !detail.preFinishTime.isNullOrEmpty() -> parts.add("要求完成:${formatTimeForTts(detail.preFinishTime!!)}") - !detail.expireTime.isNullOrEmpty() -> parts.add("截止时间:${formatTimeForTts(detail.expireTime!!)}") - } - if (detail.point > 0) parts.add("积分:${detail.point.toInt()}分") - val note = when { - !detail.taskRequire.isNullOrEmpty() -> detail.taskRequire - detail.content.isNotEmpty() -> detail.content - else -> null - } - if (!note.isNullOrBlank()) parts.add("备注:$note") - - if (parts.isEmpty()) return - - val text = parts.joinToString(",") - Timber.d("TTS 播报: $text") - - // 更新按钮状态为播放中 - updateTtsButton(playing = true) - - // 播放完成回调:恢复按钮 - edgeTtsManager.onComplete = { - activity?.runOnUiThread { updateTtsButton(playing = false) } - } - - edgeTtsManager.speak(text) { errorMsg -> - Timber.w("TTS 播报失败: $errorMsg") - updateTtsButton(playing = false) - } - } - - /** 停止播报 */ - private fun stopTts() { - edgeTtsManager.stop() - updateTtsButton(playing = false) - } - - /** - * 将时间字符串转为适合朗读的中文格式 - * "04.28 09:10" → "4月28日9点10分" - * "2026-04-28 09:10:00" → "4月28日9点10分" - */ - private fun formatTimeForTts(time: String): String { - try { - val shortPattern = Regex("""(\d{1,2})\.(\d{1,2})\s+(\d{1,2}):(\d{2})""") - shortPattern.find(time)?.let { match -> - val (month, day, hour, minute) = match.destructured - return "${month.toInt()}月${day.toInt()}日${hour.toInt()}点${minute}分" - } - val longPattern = Regex("""(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})""") - longPattern.find(time)?.let { match -> - val (_, month, day, hour, minute) = match.destructured - return "${month.toInt()}月${day.toInt()}日${hour.toInt()}点${minute}分" - } - } catch (_: Exception) { } - return time - } - - /** 更新 TTS 按钮的 UI 状态 */ - private fun updateTtsButton(playing: Boolean) { - binding.btnTts.setImageResource( - if (playing) R.drawable.ic_speaker_stop else R.drawable.ic_speaker - ) - binding.btnTts.setBackgroundResource( - if (playing) R.drawable.bg_tts_button_active else R.drawable.bg_tts_button - ) - } - - override fun onDestroyView() { - // 离开页面自动停止播放 - edgeTtsManager.stop() - edgeTtsManager.onComplete = null - super.onDestroyView() - } } diff --git a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt index 27e529d..d35cf15 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt @@ -19,7 +19,6 @@ import com.xiaoqu.watch.databinding.FragmentTaskListBinding import com.xiaoqu.watch.network.ApiResult import com.xiaoqu.watch.network.api.TaskApi import com.xiaoqu.watch.network.safeApiCall -import com.xiaoqu.watch.service.manager.EdgeTtsManager import com.xiaoqu.watch.service.manager.NfcTaskManager import com.xiaoqu.watch.ui.common.BaseFragment import com.xiaoqu.watch.ui.widget.QuTipDialog @@ -43,7 +42,6 @@ class TaskListFragment : BaseFragment() { @Inject lateinit var taskApi: TaskApi @Inject lateinit var nfcTaskManager: NfcTaskManager - @Inject lateinit var edgeTtsManager: EdgeTtsManager /** 任务 ID 列表(queryTaskIds 返回) */ @@ -119,17 +117,12 @@ class TaskListFragment : BaseFragment() { false // 不拦截,让 ScrollView 继续处理滚动 } - // TTS 播放按钮 - binding.btnTts.setOnClickListener { toggleTts() } - // 加载任务列表 fetchTaskIds() } override fun onDestroyView() { stopVoice() - edgeTtsManager.stop() - edgeTtsManager.onComplete = null super.onDestroyView() } @@ -244,7 +237,6 @@ class TaskListFragment : BaseFragment() { /** 下一个任务(左滑,内容从右滑入) */ private fun nextTask() { if (taskIndex < taskList.size - 1) { - stopTts() // 切换任务时停止播报 taskIndex++ animateSwitch(fromRight = true) } @@ -253,7 +245,6 @@ class TaskListFragment : BaseFragment() { /** 上一个任务(右滑,内容从左滑入) */ private fun prevTask() { if (taskIndex > 0) { - stopTts() // 切换任务时停止播报 taskIndex-- animateSwitch(fromRight = false) } @@ -1015,97 +1006,4 @@ class TaskListFragment : BaseFragment() { binding.btnAction.visibility = View.GONE binding.tvTitle.text = statusTitle() } - - // ===== TTS 语音播报 ===== - - /** 切换播放/停止 */ - private fun toggleTts() { - if (edgeTtsManager.isPlaying) { - stopTts() - } else { - startTts() - } - } - - /** 开始播报当前任务内容 */ - private fun startTts() { - val detail = currentDetail ?: return - - // 拼接播报文本:有值的字段才读,空字段跳过 - val parts = mutableListOf() - if (detail.name.isNotBlank()) parts.add("任务:${detail.displayName}") - if (detail.positionText.isNotBlank()) parts.add("地点:${detail.positionText}") - if (detail.sendTime.isNotBlank()) parts.add("派单时间:${formatTimeForTts(detail.sendTime)}") - // 要求完成时间 / 截止时间(优先要求,和页面显示逻辑一致) - when { - !detail.preFinishTime.isNullOrEmpty() -> parts.add("要求完成:${formatTimeForTts(detail.preFinishTime!!)}") - !detail.expireTime.isNullOrEmpty() -> parts.add("截止时间:${formatTimeForTts(detail.expireTime!!)}") - } - if (detail.point > 0) parts.add("积分:${detail.point.toInt()}分") - // 备注:优先 taskRequire,没有则用 content(和页面显示逻辑一致) - val note = when { - !detail.taskRequire.isNullOrEmpty() -> detail.taskRequire - detail.content.isNotEmpty() -> detail.content - else -> null - } - if (!note.isNullOrBlank()) parts.add("备注:$note") - - if (parts.isEmpty()) return - - val text = parts.joinToString(",") - Timber.d("TTS 播报: $text") - - // 更新按钮状态为播放中 - updateTtsButton(playing = true) - - // 播放完成回调:恢复按钮 - edgeTtsManager.onComplete = { - activity?.runOnUiThread { updateTtsButton(playing = false) } - } - - edgeTtsManager.speak(text) { errorMsg -> - Timber.w("TTS 播报失败: $errorMsg") - activity?.runOnUiThread { updateTtsButton(playing = false) } - } - } - - /** 停止播报 */ - private fun stopTts() { - edgeTtsManager.stop() - updateTtsButton(playing = false) - } - - /** - * 将时间字符串转为适合朗读的中文格式 - * "04.28 09:10" → "4月28日9点10分" - * "2026-04-28 09:10:00" → "4月28日9点10分" - * 无法解析时返回原文 - */ - private fun formatTimeForTts(time: String): String { - try { - // 匹配 "MM.dd HH:mm" 格式 - val shortPattern = Regex("""(\d{1,2})\.(\d{1,2})\s+(\d{1,2}):(\d{2})""") - shortPattern.find(time)?.let { match -> - val (month, day, hour, minute) = match.destructured - return "${month.toInt()}月${day.toInt()}日${hour.toInt()}点${minute}分" - } - // 匹配 "yyyy-MM-dd HH:mm:ss" 或 "yyyy-MM-dd HH:mm" 格式 - val longPattern = Regex("""(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})""") - longPattern.find(time)?.let { match -> - val (_, month, day, hour, minute) = match.destructured - return "${month.toInt()}月${day.toInt()}日${hour.toInt()}点${minute}分" - } - } catch (_: Exception) { } - return time - } - - /** 更新 TTS 按钮的 UI 状态 */ - private fun updateTtsButton(playing: Boolean) { - binding.btnTts.setImageResource( - if (playing) R.drawable.ic_speaker_stop else R.drawable.ic_speaker - ) - binding.btnTts.setBackgroundResource( - if (playing) R.drawable.bg_tts_button_active else R.drawable.bg_tts_button - ) - } } diff --git a/app/src/main/res/drawable/bg_tts_button.xml b/app/src/main/res/drawable/bg_tts_button.xml deleted file mode 100644 index f77c685..0000000 --- a/app/src/main/res/drawable/bg_tts_button.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/bg_tts_button_active.xml b/app/src/main/res/drawable/bg_tts_button_active.xml deleted file mode 100644 index b88c5ba..0000000 --- a/app/src/main/res/drawable/bg_tts_button_active.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_speaker.xml b/app/src/main/res/drawable/ic_speaker.xml deleted file mode 100644 index 0e849e0..0000000 --- a/app/src/main/res/drawable/ic_speaker.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_speaker_stop.xml b/app/src/main/res/drawable/ic_speaker_stop.xml deleted file mode 100644 index 3f41a81..0000000 --- a/app/src/main/res/drawable/ic_speaker_stop.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_task_detail.xml b/app/src/main/res/layout/fragment_task_detail.xml index 6b8fb59..0801f49 100644 --- a/app/src/main/res/layout/fragment_task_detail.xml +++ b/app/src/main/res/layout/fragment_task_detail.xml @@ -101,18 +101,6 @@ - - - - - - -