package com.xuqm.sdk.update import android.content.Context import android.util.Log import com.google.gson.Gson import com.xuqm.sdk.core.ServiceEndpointRegistry import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener import java.util.concurrent.TimeUnit /** * WebSocket 客户端,用于接收版本发布的实时通知。 * * 工作流程: * 1. 服务端发布新版本时,通过 WebSocket 发送轻量通知 * 2. SDK 收到通知后自动调用 [UpdateSDK.checkAppUpdate](尊重灰度发布规则) * 3. 根据检查结果回调 [UpdateListener] * * App 只需注册监听并调用 [connect],无需关心底层通信细节。 * * 使用方式: * ``` * // 创建并注册监听 * val ws = UpdateSDK.createWebSocket(context, object : UpdateListener { * override fun onUpdateAvailable(updateInfo: UpdateInfo) { * showUpdateDialog(updateInfo) * } * override fun onNoUpdate() { } * override fun onError(error: Throwable) { } * }) * ws.connect() * * // 页面销毁时 * ws.disconnect() * ``` */ class UpdateWebSocket internal constructor( private val context: Context, private val appKey: String, private val listener: UpdateListener, ) { companion object { private const val TAG = "UpdateWebSocket" private const val RECONNECT_DELAY_MS = 5_000L private const val MAX_RECONNECT_DELAY_MS = 300_000L // 5 minutes } private val gson = Gson() private val client = OkHttpClient.Builder() .pingInterval(30, TimeUnit.SECONDS) .build() private var webSocket: WebSocket? = null @Volatile private var connected = false @Volatile private var manuallyClosed = false @Volatile private var reconnectDelay = RECONNECT_DELAY_MS fun isConnected(): Boolean = connected /** * 建立 WebSocket 连接并注册监听。 * 如果已连接,不会重复连接。 */ fun connect() { if (connected) return manuallyClosed = false reconnectDelay = RECONNECT_DELAY_MS doConnect() } /** * 断开 WebSocket 连接并移除监听。 * 调用后不会自动重连。 */ fun disconnect() { manuallyClosed = true webSocket?.close(1000, "Client closing") webSocket = null connected = false } private fun doConnect() { val baseUrl = ServiceEndpointRegistry.updateBaseUrl .replace("^https://".toRegex(), "wss://") .replace("^http://".toRegex(), "ws://") .trimEnd('/') val url = "$baseUrl/ws/updates?appKey=$appKey" Log.d(TAG, "Connecting to $url") val request = Request.Builder() .url(url) .build() webSocket = client.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { Log.d(TAG, "Connected") connected = true reconnectDelay = RECONNECT_DELAY_MS } override fun onMessage(webSocket: WebSocket, text: String) { Log.d(TAG, "Received: $text") handleMessage(text) } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { Log.d(TAG, "Server closing: $code $reason") webSocket.close(code, reason) } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { Log.d(TAG, "Closed: $code $reason") connected = false scheduleReconnect() } override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { Log.w(TAG, "Connection failed: ${t.message}") connected = false scheduleReconnect() } }) } /** * 处理服务端推送的消息。 * 收到 "new_version_available" 事件后,自动调用 checkUpdate 接口。 */ private fun handleMessage(text: String) { try { val json = gson.fromJson(text, Map::class.java) val event = json["event"] as? String ?: return if (event == "new_version_available") { Log.d(TAG, "New version notification received, checking update...") checkUpdateAndNotify() } } catch (e: Exception) { Log.w(TAG, "Failed to parse message: ${e.message}") } } /** * 调用 checkUpdate 接口,根据结果回调 listener。 * 此方法在 IO 线程执行,回调在主线程执行。 */ private fun checkUpdateAndNotify() { Thread { try { val updateInfo = kotlinx.coroutines.runBlocking { UpdateSDK.checkAppUpdate(context, bypassIgnore = false) } android.os.Handler(android.os.Looper.getMainLooper()).post { if (updateInfo != null && updateInfo.needsUpdate) { Log.d(TAG, "Update available: ${updateInfo.versionName} (${updateInfo.versionCode})") listener.onUpdateAvailable(updateInfo) } else { Log.d(TAG, "No update available") listener.onNoUpdate() } } } catch (e: Exception) { Log.w(TAG, "checkUpdate failed: ${e.message}") android.os.Handler(android.os.Looper.getMainLooper()).post { listener.onError(e) } } }.start() } private fun scheduleReconnect() { if (manuallyClosed) return Log.d(TAG, "Reconnecting in ${reconnectDelay}ms") Thread { Thread.sleep(reconnectDelay) if (!manuallyClosed && !connected) { reconnectDelay = (reconnectDelay * 2).coerceAtMost(MAX_RECONNECT_DELAY_MS) doConnect() } }.start() } }