feat: 消息通知模块(横幅+红点+跳转)
MQTT type=1 新任务推送 → 震动+亮屏+顶部蓝色横幅+卡片红点。 新增: - NotificationManager: 去抖1s+内存存储taskIds+统计对比红点 - NotificationBannerView: Activity层横幅(滑入/10s倒计时/点击) - AppEvent.NewTaskArrived: 携带taskIds和count 集成: - MainActivity: 监听MQTT type=1→NotificationManager→横幅 - HomeFragment: 监听NewTaskArrived→刷新统计+对比红点+横幅点击跳转 - page_main.xml: 3个卡片各加红点角标(FrameLayout包裹) - nav_main.xml: 新增action_home_to_taskDetail Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
package com.xiaoqu.watch.service.manager
|
||||
|
||||
import com.xiaoqu.watch.data.task.TaskStatistics
|
||||
import com.xiaoqu.watch.device.screen.ScreenController
|
||||
import com.xiaoqu.watch.device.sensor.VibrationController
|
||||
import com.xiaoqu.watch.device.sensor.VibrationDefaults
|
||||
import com.xiaoqu.watch.event.AppEvent
|
||||
import com.xiaoqu.watch.event.EventBus
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* 消息通知管理器
|
||||
*
|
||||
* 职责:
|
||||
* - 处理 MQTT type=1 新任务消息(去抖 + 解析任务 ID)
|
||||
* - 震动 + 亮屏反馈
|
||||
* - 管理未读任务 ID 列表(内存,不持久化)
|
||||
* - 对比统计数字变化,确定红点卡片
|
||||
*/
|
||||
@Singleton
|
||||
class NotificationManager @Inject constructor(
|
||||
private val vibrationController: VibrationController,
|
||||
private val screenController: ScreenController,
|
||||
private val eventBus: EventBus
|
||||
) {
|
||||
companion object {
|
||||
/** 去抖间隔(毫秒) */
|
||||
private const val DEBOUNCE_MS = 1000L
|
||||
/** 新消息震动方案 */
|
||||
private const val PLAN_NEW_MESSAGE = 2
|
||||
}
|
||||
|
||||
/** 未读的新任务 ID 列表 */
|
||||
private val _pendingTaskIds = mutableListOf<String>()
|
||||
val pendingTaskIds: List<String> get() = _pendingTaskIds.toList()
|
||||
|
||||
/** 未读任务数量 */
|
||||
val pendingCount: Int get() = _pendingTaskIds.size
|
||||
|
||||
/** 上次消息处理时间(去抖用) */
|
||||
private var lastMessageTime = 0L
|
||||
|
||||
/** 上次统计数据(对比红点用) */
|
||||
var lastStats: TaskStatistics? = null
|
||||
|
||||
/** 协程作用域 */
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
/**
|
||||
* 处理 MQTT type=1 新任务消息
|
||||
* @param rawJson MQTT 消息原始 JSON
|
||||
* @return true=已处理,false=被去抖过滤
|
||||
*/
|
||||
fun onNewTaskMessage(rawJson: String): Boolean {
|
||||
// 1. 去抖:1s 内重复消息忽略
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastMessageTime < DEBOUNCE_MS) {
|
||||
Timber.d("通知: 去抖过滤 (距上条 ${now - lastMessageTime}ms)")
|
||||
return false
|
||||
}
|
||||
lastMessageTime = now
|
||||
|
||||
// 2. 解析任务 ID
|
||||
val taskIds = parseTaskIds(rawJson)
|
||||
if (taskIds.isEmpty()) {
|
||||
Timber.w("通知: 消息中无有效任务 ID")
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. 加入未读列表(去重)
|
||||
for (id in taskIds) {
|
||||
if (id !in _pendingTaskIds) {
|
||||
_pendingTaskIds.add(id)
|
||||
}
|
||||
}
|
||||
Timber.d("通知: 收到 ${taskIds.size} 个新任务, 当前未读 ${_pendingTaskIds.size}")
|
||||
|
||||
// 4. 震动 + 亮屏
|
||||
val pattern = VibrationDefaults.getPattern(PLAN_NEW_MESSAGE)
|
||||
if (pattern != null) {
|
||||
vibrationController.executePattern(pattern)
|
||||
}
|
||||
screenController.turnOn()
|
||||
|
||||
// 5. 发送事件通知
|
||||
scope.launch {
|
||||
eventBus.emit(AppEvent.NewTaskArrived(taskIds, _pendingTaskIds.size))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/** 消费所有未读消息(用户已查看列表) */
|
||||
fun consumeAll() {
|
||||
_pendingTaskIds.clear()
|
||||
Timber.d("通知: 已清空全部未读")
|
||||
}
|
||||
|
||||
/** 按 ID 消费(任务完成时) */
|
||||
fun consumeByTaskId(taskId: String) {
|
||||
_pendingTaskIds.remove(taskId)
|
||||
Timber.d("通知: 已消费任务 $taskId, 剩余 ${_pendingTaskIds.size}")
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比统计数字变化,返回需要加红点的卡片 tableStatus 集合
|
||||
* @param oldStats 旧统计
|
||||
* @param newStats 新统计
|
||||
* @return 数字增加的 tableStatus 集合(2=接单池, 3=待打卡, 4=待完成)
|
||||
*/
|
||||
fun diffStats(oldStats: TaskStatistics?, newStats: TaskStatistics): Set<Int> {
|
||||
if (oldStats == null) return emptySet()
|
||||
val result = mutableSetOf<Int>()
|
||||
if (newStats.waitForTask > oldStats.waitForTask) result.add(2)
|
||||
if (newStats.treatTask > oldStats.treatTask) result.add(3)
|
||||
if (newStats.incompleteTask > oldStats.incompleteTask) result.add(4)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 MQTT JSON 中解析任务 ID 列表
|
||||
* 消息格式待实际验证,先按旧版推断(消息对象含 id 字段或 taskArr 数组)
|
||||
*/
|
||||
private fun parseTaskIds(rawJson: String): List<String> {
|
||||
return try {
|
||||
val json = JSONObject(rawJson)
|
||||
|
||||
// 尝试方式1:消息本身就是一个任务(含 id 字段)
|
||||
val directId = json.optString("id", "")
|
||||
if (directId.isNotEmpty()) {
|
||||
return listOf(directId)
|
||||
}
|
||||
|
||||
// 尝试方式2:消息含 taskArr 数组
|
||||
val taskArr = json.optJSONArray("taskArr")
|
||||
if (taskArr != null) {
|
||||
val ids = mutableListOf<String>()
|
||||
for (i in 0 until taskArr.length()) {
|
||||
val taskId = taskArr.optJSONObject(i)?.optString("id", "") ?: ""
|
||||
if (taskId.isNotEmpty()) ids.add(taskId)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// 尝试方式3:消息含 data 数组
|
||||
val dataArr = json.optJSONArray("data")
|
||||
if (dataArr != null) {
|
||||
val ids = mutableListOf<String>()
|
||||
for (i in 0 until dataArr.length()) {
|
||||
val taskId = dataArr.optJSONObject(i)?.optString("id", "") ?: ""
|
||||
if (taskId.isNotEmpty()) ids.add(taskId)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
emptyList()
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "通知: 解析任务 ID 失败")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user