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 @@
-
-
-
-
-
-
-