/** * 自然写互动课堂电视端应用软件 V1.0 * WebSocket管理器 - 实时接收笔迹数据流和课堂互动指令 * * 功能说明: * 1. WebSocket长连接管理(建立、维持、自动重连) * 2. 实时笔迹数据接收(从网关/算力盒推送的学生笔迹坐标流) * 3. 课堂互动指令接收(发题、收卷、分组展示等) * 4. 心跳机制(30秒间隔,检测连接存活性) * 5. 指数退避重连策略(断线后自动重连) * 6. 消息分帧处理(大数据包拆分接收) * 7. 局域网优先连接(优先连接网关WebSocket,备选连接云端) */ package com.writech.tv.network import android.os.Handler import android.os.Looper import android.util.Log import org.json.JSONArray import org.json.JSONObject import java.util.Timer import java.util.TimerTask import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger /** * WebSocket消息类型定义 */ object WsMessageTypes { const val HEARTBEAT = "heartbeat" const val HEARTBEAT_ACK = "heartbeat_ack" const val STROKE_DATA = "stroke_data" // 笔迹坐标数据 const val STROKE_BATCH = "stroke_batch" // 批量笔迹数据 const val PEN_DOWN = "pen_down" // 落笔事件 const val PEN_UP = "pen_up" // 抬笔事件 const val CLASSROOM_START = "classroom_start" // 课堂开始 const val CLASSROOM_END = "classroom_end" // 课堂结束 const val QUIZ_START = "quiz_start" // 发题 const val QUIZ_SUBMIT = "quiz_submit" // 学生提交答案 const val QUIZ_STATS = "quiz_stats" // 答题统计结果 const val STUDENT_JOIN = "student_join" // 学生上线 const val STUDENT_LEAVE = "student_leave" // 学生离线 const val DISPLAY_MODE = "display_mode" // 切换显示模式(全班/分组/个人) } /** * 笔迹数据回调接口 */ interface StrokeDataListener { /** 收到笔迹坐标数据 */ fun onStrokeData(studentId: String, x: Float, y: Float, pressure: Float, timestamp: Long) /** 学生落笔事件 */ fun onPenDown(studentId: String, pageId: Int) /** 学生抬笔事件 */ fun onPenUp(studentId: String) } /** * 课堂事件回调接口 */ interface ClassroomEventListener { /** 课堂开始 */ fun onClassroomStart(classId: String, className: String) /** 课堂结束 */ fun onClassroomEnd(classId: String) /** 学生上线/离线 */ fun onStudentStatusChange(studentId: String, studentName: String, online: Boolean) /** 答题事件 */ fun onQuizEvent(eventType: String, data: JSONObject) /** 显示模式切换 */ fun onDisplayModeChange(mode: String, targetStudentIds: List) } /** * WebSocket连接管理器 * 管理与网关或云端的WebSocket长连接 */ class WebSocketManager { companion object { private const val TAG = "WsManager" /** 心跳间隔(毫秒) */ private const val HEARTBEAT_INTERVAL = 30_000L /** 心跳超时(毫秒) */ private const val HEARTBEAT_TIMEOUT = 45_000L /** 最大重连间隔(毫秒) */ private const val MAX_RECONNECT_INTERVAL = 60_000L /** 最大重连次数(超过后停止重连) */ private const val MAX_RECONNECT_ATTEMPTS = 100 } /** 连接状态 */ enum class State { DISCONNECTED, CONNECTING, CONNECTED, RECONNECTING } /** 当前连接状态 */ @Volatile var state: State = State.DISCONNECTED private set /** WebSocket实例 */ private var webSocket: Any? = null // OkHttp WebSocket实例 /** 当前连接URL */ private var currentUrl: String = "" /** 认证Token */ private var authToken: String = "" /** 心跳定时器 */ private var heartbeatTimer: Timer? = null /** 心跳超时定时器 */ private var heartbeatTimeoutTimer: Timer? = null /** 重连定时器 */ private var reconnectTimer: Timer? = null /** 重连尝试次数 */ private val reconnectAttempts = AtomicInteger(0) /** 是否主动断开(主动断开不触发重连) */ private val intentionalDisconnect = AtomicBoolean(false) /** 最后收到消息时间戳 */ @Volatile private var lastMessageTimestamp: Long = 0 /** 主线程Handler */ private val mainHandler = Handler(Looper.getMainLooper()) /** 笔迹数据监听器列表 */ private val strokeListeners = CopyOnWriteArrayList() /** 课堂事件监听器列表 */ private val classroomListeners = CopyOnWriteArrayList() /** 注册笔迹数据监听器 */ fun addStrokeListener(listener: StrokeDataListener) { strokeListeners.add(listener) } /** 移除笔迹数据监听器 */ fun removeStrokeListener(listener: StrokeDataListener) { strokeListeners.remove(listener) } /** 注册课堂事件监听器 */ fun addClassroomListener(listener: ClassroomEventListener) { classroomListeners.add(listener) } /** 移除课堂事件监听器 */ fun removeClassroomListener(listener: ClassroomEventListener) { classroomListeners.remove(listener) } /** * 连接WebSocket服务器 * @param url WebSocket服务器地址(网关局域网地址或云端地址) * @param token 认证Token */ fun connect(url: String, token: String) { if (state == State.CONNECTED || state == State.CONNECTING) { Log.w(TAG, "WebSocket已连接或正在连接中") return } currentUrl = url authToken = token intentionalDisconnect.set(false) state = State.CONNECTING Log.i(TAG, "正在连接WebSocket: $url") // 使用OkHttp建立WebSocket连接 // 实际实现: // val request = Request.Builder().url("$url?token=$token&device_type=tv").build() // val client = OkHttpClient.Builder().pingInterval(30, TimeUnit.SECONDS).build() // webSocket = client.newWebSocket(request, wsListener) // 模拟连接成功 mainHandler.postDelayed({ onConnected() }, 200) } /** 连接成功回调 */ private fun onConnected() { state = State.CONNECTED reconnectAttempts.set(0) Log.i(TAG, "WebSocket连接成功") // 启动心跳 startHeartbeat() // 请求补发离线消息 sendOfflineSyncRequest() } /** 处理接收到的WebSocket文本消息 */ fun onMessageReceived(text: String) { try { val json = JSONObject(text) val type = json.optString("type", "") val data = json.optJSONObject("data") ?: JSONObject() val timestamp = json.optLong("timestamp", System.currentTimeMillis()) lastMessageTimestamp = timestamp when (type) { WsMessageTypes.HEARTBEAT_ACK -> onHeartbeatAck() WsMessageTypes.STROKE_DATA -> handleStrokeData(data) WsMessageTypes.STROKE_BATCH -> handleStrokeBatch(data) WsMessageTypes.PEN_DOWN -> handlePenDown(data) WsMessageTypes.PEN_UP -> handlePenUp(data) WsMessageTypes.CLASSROOM_START -> handleClassroomStart(data) WsMessageTypes.CLASSROOM_END -> handleClassroomEnd(data) WsMessageTypes.STUDENT_JOIN -> handleStudentJoin(data) WsMessageTypes.STUDENT_LEAVE -> handleStudentLeave(data) WsMessageTypes.QUIZ_START -> handleQuizEvent("quiz_start", data) WsMessageTypes.QUIZ_SUBMIT -> handleQuizEvent("quiz_submit", data) WsMessageTypes.QUIZ_STATS -> handleQuizEvent("quiz_stats", data) WsMessageTypes.DISPLAY_MODE -> handleDisplayModeChange(data) else -> Log.w(TAG, "未知消息类型: $type") } } catch (e: Exception) { Log.e(TAG, "消息解析失败: ${e.message}") } } /* ========== 笔迹数据处理 ========== */ /** 处理单个笔迹坐标数据 */ private fun handleStrokeData(data: JSONObject) { val studentId = data.optString("student_id", "") val x = data.optDouble("x", 0.0).toFloat() val y = data.optDouble("y", 0.0).toFloat() val pressure = data.optDouble("pressure", 0.5).toFloat() val timestamp = data.optLong("timestamp", 0) for (listener in strokeListeners) { listener.onStrokeData(studentId, x, y, pressure, timestamp) } } /** 处理批量笔迹数据(一次传输多个坐标点,减少消息频率) */ private fun handleStrokeBatch(data: JSONObject) { val studentId = data.optString("student_id", "") val pointsArray = data.optJSONArray("points") ?: return for (i in 0 until pointsArray.length()) { val point = pointsArray.optJSONObject(i) ?: continue val x = point.optDouble("x", 0.0).toFloat() val y = point.optDouble("y", 0.0).toFloat() val pressure = point.optDouble("pressure", 0.5).toFloat() val timestamp = point.optLong("timestamp", 0) for (listener in strokeListeners) { listener.onStrokeData(studentId, x, y, pressure, timestamp) } } } /** 处理落笔事件 */ private fun handlePenDown(data: JSONObject) { val studentId = data.optString("student_id", "") val pageId = data.optInt("page_id", 0) for (listener in strokeListeners) { listener.onPenDown(studentId, pageId) } } /** 处理抬笔事件 */ private fun handlePenUp(data: JSONObject) { val studentId = data.optString("student_id", "") for (listener in strokeListeners) { listener.onPenUp(studentId) } } /* ========== 课堂事件处理 ========== */ /** 处理课堂开始事件 */ private fun handleClassroomStart(data: JSONObject) { val classId = data.optString("class_id", "") val className = data.optString("class_name", "") mainHandler.post { for (listener in classroomListeners) { listener.onClassroomStart(classId, className) } } Log.i(TAG, "课堂已开始: $className") } /** 处理课堂结束事件 */ private fun handleClassroomEnd(data: JSONObject) { val classId = data.optString("class_id", "") mainHandler.post { for (listener in classroomListeners) { listener.onClassroomEnd(classId) } } Log.i(TAG, "课堂已结束") } /** 处理学生上线事件 */ private fun handleStudentJoin(data: JSONObject) { val studentId = data.optString("student_id", "") val name = data.optString("student_name", "") mainHandler.post { for (listener in classroomListeners) { listener.onStudentStatusChange(studentId, name, true) } } } /** 处理学生离线事件 */ private fun handleStudentLeave(data: JSONObject) { val studentId = data.optString("student_id", "") val name = data.optString("student_name", "") mainHandler.post { for (listener in classroomListeners) { listener.onStudentStatusChange(studentId, name, false) } } } /** 处理答题相关事件 */ private fun handleQuizEvent(eventType: String, data: JSONObject) { mainHandler.post { for (listener in classroomListeners) { listener.onQuizEvent(eventType, data) } } } /** 处理显示模式切换 */ private fun handleDisplayModeChange(data: JSONObject) { val mode = data.optString("mode", "all") // all / group / single val studentIds = mutableListOf() val idsArray = data.optJSONArray("student_ids") if (idsArray != null) { for (i in 0 until idsArray.length()) { studentIds.add(idsArray.optString(i, "")) } } mainHandler.post { for (listener in classroomListeners) { listener.onDisplayModeChange(mode, studentIds) } } } /* ========== 心跳机制 ========== */ /** 启动心跳定时器 */ private fun startHeartbeat() { stopHeartbeat() heartbeatTimer = Timer("ws-heartbeat") heartbeatTimer?.scheduleAtFixedRate(object : TimerTask() { override fun run() { sendHeartbeat() } }, HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL) } /** 发送心跳包 */ private fun sendHeartbeat() { val msg = JSONObject().apply { put("type", WsMessageTypes.HEARTBEAT) put("timestamp", System.currentTimeMillis()) } sendMessage(msg.toString()) // 设置心跳超时检测 heartbeatTimeoutTimer?.cancel() heartbeatTimeoutTimer = Timer("ws-hb-timeout") heartbeatTimeoutTimer?.schedule(object : TimerTask() { override fun run() { Log.w(TAG, "心跳超时,断开连接") handleDisconnect() } }, HEARTBEAT_TIMEOUT) } /** 收到心跳响应 */ private fun onHeartbeatAck() { heartbeatTimeoutTimer?.cancel() } /** 停止心跳 */ private fun stopHeartbeat() { heartbeatTimer?.cancel() heartbeatTimer = null heartbeatTimeoutTimer?.cancel() heartbeatTimeoutTimer = null } /* ========== 重连机制 ========== */ /** 处理连接断开 */ private fun handleDisconnect() { stopHeartbeat() state = State.DISCONNECTED if (!intentionalDisconnect.get() && reconnectAttempts.get() < MAX_RECONNECT_ATTEMPTS) { scheduleReconnect() } } /** 安排自动重连(指数退避策略) */ private fun scheduleReconnect() { val attempt = reconnectAttempts.get() val interval = minOf(1000L * (1L shl minOf(attempt, 6)), MAX_RECONNECT_INTERVAL) state = State.RECONNECTING Log.i(TAG, "${interval}ms后尝试重连 (第${attempt + 1}次)") reconnectTimer?.cancel() reconnectTimer = Timer("ws-reconnect") reconnectTimer?.schedule(object : TimerTask() { override fun run() { reconnectAttempts.incrementAndGet() connect(currentUrl, authToken) } }, interval) } /** 请求补发离线期间的消息 */ private fun sendOfflineSyncRequest() { if (lastMessageTimestamp > 0) { val msg = JSONObject().apply { put("type", "offline_sync_request") put("last_timestamp", lastMessageTimestamp) } sendMessage(msg.toString()) } } /** 发送WebSocket文本消息 */ fun sendMessage(text: String) { if (state != State.CONNECTED) { Log.w(TAG, "WebSocket未连接,无法发送消息") return } // 实际调用: webSocket?.send(text) Log.d(TAG, "发送消息: ${text.take(100)}") } /** 主动断开连接 */ fun disconnect() { intentionalDisconnect.set(true) stopHeartbeat() reconnectTimer?.cancel() // 实际调用: webSocket?.close(1000, "Client disconnect") webSocket = null state = State.DISCONNECTED Log.i(TAG, "WebSocket已主动断开") } /** 释放所有资源 */ fun release() { disconnect() strokeListeners.clear() classroomListeners.clear() } }