188 行
6.0 KiB
Kotlin
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()
|
|||
|
|
}
|
|||
|
|
}
|