XuqmGroup-AndroidSDK/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateWebSocket.kt

188 行
6.0 KiB
Kotlin

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()
}
}