From 570de3c4e78772bb3144aed23b7051c835dc2ea2 Mon Sep 17 00:00:00 2001 From: yangxisong Date: Wed, 12 Nov 2025 18:20:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BD=91=E7=BB=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 5 +- app/src/main/AndroidManifest.xml | 8 ++ .../java/com/yuanxuan/rokid/MainActivity.kt | 36 ++++- .../com/yuanxuan/rokid/RokidApplication.kt | 11 +- .../rokid/dependencies/AppDependencies.kt | 21 +++ .../ApplicationDependencyProvider.kt | 23 ++- .../rokid/device/DeviceServiceManager.kt | 9 +- .../com/yuanxuan/rokid/network/NetUtils.kt | 38 +++++ .../rokid/network/http/ApiRepository.kt | 10 ++ .../rokid/network/http/ApiResponse.kt | 14 ++ .../yuanxuan/rokid/network/http/ApiService.kt | 12 ++ .../rokid/network/http/NetworkException.kt | 5 + .../rokid/network/http/OkHttpManager.kt | 135 ++++++++++++++++++ .../yuanxuan/rokid/network/http/RequestApi.kt | 13 ++ .../rokid/network/http/RetrofitClient.kt | 37 +++++ .../websocket/OkHttpWebSocketConnection.kt | 72 ++++++++++ .../network/websocket/WebSocketConnection.kt | 28 ++++ .../websocket/WebSocketConnectionState.java | 15 ++ .../network/websocket/WebSocketManager.kt | 51 +++++++ .../websocket/WebSocketReconnectWorker.kt | 44 ++++++ .../websocket/WebSocketRequestMessage.kt | 5 + .../websocket/WebSocketResponseMessage.kt | 5 + .../main/res/xml/network_security_config.xml | 7 + gradle/libs.versions.toml | 7 + 24 files changed, 600 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/NetUtils.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/http/ApiRepository.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/http/ApiResponse.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/http/ApiService.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/http/NetworkException.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/http/OkHttpManager.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/http/RequestApi.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/http/RetrofitClient.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/websocket/OkHttpWebSocketConnection.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketConnection.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketConnectionState.java create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketManager.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketReconnectWorker.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketRequestMessage.kt create mode 100644 app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketResponseMessage.kt create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e1f0285..94062d2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -45,8 +45,11 @@ dependencies { implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) implementation(libs.logger.timber) - implementation(libs.okhttp) implementation(libs.okhttp.logging.interceptor) + implementation(libs.androidx.work.ktx) + implementation(libs.retrofit) + implementation(libs.retrofit.converter.gson) +// implementation(libs.gson) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c0f5076..c0f97ba 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/MainActivity.kt b/app/src/main/java/com/yuanxuan/rokid/MainActivity.kt index 587983b..8954d56 100644 --- a/app/src/main/java/com/yuanxuan/rokid/MainActivity.kt +++ b/app/src/main/java/com/yuanxuan/rokid/MainActivity.kt @@ -7,9 +7,20 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.lifecycleScope -import com.yuanxuan.rokid.dependencies.AppDependencies +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.yuanxuan.rokid.network.http.ApiResponse +import com.yuanxuan.rokid.network.http.Test +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import timber.log.Timber +import java.lang.reflect.Type class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -21,9 +32,26 @@ class MainActivity : AppCompatActivity() { v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } - AppDependencies.deviceServiceManager.playTTS("测试一下") + val json = "{\n" + + " \"code\": 0,\n" + + " \"msg\": \"success\",\n" + + " \"data\": {\n" + + " \"userId\": \"123\",\n" + + " \"name\": \"Alice\"\n" + + " }\n" + + "}" + + val x = test(json) + +// onBackPressedDispatcher.addCallback { +// } + } + + private inline fun test(responseBody: String): T { + val typeToken = object : TypeToken>() {} + val typeOfT: Type = typeToken.type + val apiResponse: ApiResponse = Gson().fromJson(responseBody, typeOfT) + return apiResponse.data - onBackPressedDispatcher.addCallback { - } } } \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/RokidApplication.kt b/app/src/main/java/com/yuanxuan/rokid/RokidApplication.kt index c3b895a..459d56a 100644 --- a/app/src/main/java/com/yuanxuan/rokid/RokidApplication.kt +++ b/app/src/main/java/com/yuanxuan/rokid/RokidApplication.kt @@ -18,13 +18,18 @@ class RokidApplication : Application() { override fun onCreate() { super.onCreate() Timber.plant(Timber.DebugTree()) - AppDependencies.init(this, ApplicationDependencyProvider(this)) + AppDependencies.init(this, ApplicationDependencyProvider(this, applicationScope)) /** * 启动APP必须先获取到SN 后面网络依赖 */ applicationScope.launch { - val sn = AppDependencies.deviceServiceManager.getSn() - Timber.d("sn = $sn") + runCatching { + AppDependencies.deviceServiceManager.getSn() + }.onSuccess { + AppDependencies.webSocketManager.connect() + }.onFailure { + Timber.e(it) + } } /** * 这个服务保证心跳包准时发 diff --git a/app/src/main/java/com/yuanxuan/rokid/dependencies/AppDependencies.kt b/app/src/main/java/com/yuanxuan/rokid/dependencies/AppDependencies.kt index a5f1a48..4294d90 100644 --- a/app/src/main/java/com/yuanxuan/rokid/dependencies/AppDependencies.kt +++ b/app/src/main/java/com/yuanxuan/rokid/dependencies/AppDependencies.kt @@ -2,6 +2,12 @@ package com.yuanxuan.rokid.dependencies import android.app.Application import com.yuanxuan.rokid.device.DeviceServiceManager +import com.yuanxuan.rokid.network.http.ApiRepository +import com.yuanxuan.rokid.network.http.ApiService +import com.yuanxuan.rokid.network.http.OkHttpManager +import com.yuanxuan.rokid.network.http.RequestApi +import com.yuanxuan.rokid.network.http.RetrofitClient +import com.yuanxuan.rokid.network.websocket.WebSocketManager /** * 项目的服务管理器,所有依赖集中管理 @@ -11,6 +17,9 @@ object AppDependencies { private lateinit var _application: Application private lateinit var provider: Provider + val application: Application + get() = _application + fun init(application: Application, provider: Provider) { if (this::_application.isInitialized || this::provider.isInitialized) { return @@ -24,8 +33,20 @@ object AppDependencies { provider.provideDeviceServiceManager() } + val webSocketManager by lazy { + provider.provideWebSocketManager() + } + + val requestApi by lazy { + provider.provideApiRepository( + apiService = RetrofitClient.apiService + ) + } + interface Provider { fun provideDeviceServiceManager(): DeviceServiceManager + fun provideWebSocketManager(): WebSocketManager + fun provideApiRepository(apiService: ApiService): ApiRepository } } \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/dependencies/ApplicationDependencyProvider.kt b/app/src/main/java/com/yuanxuan/rokid/dependencies/ApplicationDependencyProvider.kt index 1483fa9..e305c42 100644 --- a/app/src/main/java/com/yuanxuan/rokid/dependencies/ApplicationDependencyProvider.kt +++ b/app/src/main/java/com/yuanxuan/rokid/dependencies/ApplicationDependencyProvider.kt @@ -2,9 +2,30 @@ package com.yuanxuan.rokid.dependencies import android.app.Application import com.yuanxuan.rokid.device.DeviceServiceManager +import com.yuanxuan.rokid.network.http.ApiRepository +import com.yuanxuan.rokid.network.http.ApiService +import com.yuanxuan.rokid.network.http.OkHttpManager +import com.yuanxuan.rokid.network.http.RequestApi +import com.yuanxuan.rokid.network.websocket.WebSocketManager +import kotlinx.coroutines.CoroutineScope -class ApplicationDependencyProvider(val context: Application) : AppDependencies.Provider { +class ApplicationDependencyProvider(val context: Application, val scope: CoroutineScope) : + AppDependencies.Provider { override fun provideDeviceServiceManager(): DeviceServiceManager { return DeviceServiceManager(context) } + + override fun provideWebSocketManager(): WebSocketManager { + return WebSocketManager( + context = context, + scope = scope + ) + } + + override fun provideApiRepository(apiService: ApiService): ApiRepository { + return ApiRepository( + apiService = apiService + ) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/device/DeviceServiceManager.kt b/app/src/main/java/com/yuanxuan/rokid/device/DeviceServiceManager.kt index 9e2cdfe..303be00 100644 --- a/app/src/main/java/com/yuanxuan/rokid/device/DeviceServiceManager.kt +++ b/app/src/main/java/com/yuanxuan/rokid/device/DeviceServiceManager.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import timber.log.Timber import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException class DeviceServiceManager(val context: Application) { @@ -157,7 +158,12 @@ class DeviceServiceManager(val context: Application) { } suspend fun getSn() = suspendCancellableCoroutine { continuation -> - systemFuncServiceTodo { service -> + ServiceManager.getSystemFuncService(context) { service -> + if (service == null) { + continuation.resumeWithException(Throwable("没有拿到SN 要重启APP")) + return@getSystemFuncService + } + systemFuncService = service sn = service.sn continuation.resume(sn) } @@ -187,7 +193,6 @@ class DeviceServiceManager(val context: Application) { } systemFuncService = service todo.invoke(service) - Timber.d(" ${service.sn}") } } else { todo.invoke(systemFuncService!!) diff --git a/app/src/main/java/com/yuanxuan/rokid/network/NetUtils.kt b/app/src/main/java/com/yuanxuan/rokid/network/NetUtils.kt new file mode 100644 index 0000000..50f1f48 --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/NetUtils.kt @@ -0,0 +1,38 @@ +package com.yuanxuan.rokid.network + +import timber.log.Timber +import java.net.Inet4Address +import java.net.NetworkInterface + +object NetUtils { + + fun getBaseUrl() = "http://${getLocalIPV4address()}.83:8765" + + + /** + * 获取局域网IP + */ + fun getLocalIPV4address(): String? { + try { + val networkInterfaces = NetworkInterface.getNetworkInterfaces() + while (networkInterfaces.hasMoreElements()) { + val networkInterface = networkInterfaces.nextElement() + val inetAddresses = networkInterface.inetAddresses + while (inetAddresses.hasMoreElements()) { + val inetAddress = inetAddresses.nextElement() + if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) { + val fullIp = inetAddress.hostAddress + if (fullIp.isNullOrEmpty()) + return null + val lastDotIndex = fullIp.lastIndexOf(".") + // 返回第一个找到的IPv4地址 + return fullIp.take(lastDotIndex) + } + } + } + } catch (e: Exception) { + Timber.e(e, "获取局域网IP地址时发生异常") + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/http/ApiRepository.kt b/app/src/main/java/com/yuanxuan/rokid/network/http/ApiRepository.kt new file mode 100644 index 0000000..7ff169f --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/http/ApiRepository.kt @@ -0,0 +1,10 @@ +package com.yuanxuan.rokid.network.http + +class ApiRepository(private val apiService: ApiService) { + + suspend fun test(): Test? { + apiService.test() + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/http/ApiResponse.kt b/app/src/main/java/com/yuanxuan/rokid/network/http/ApiResponse.kt new file mode 100644 index 0000000..24d4d58 --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/http/ApiResponse.kt @@ -0,0 +1,14 @@ +package com.yuanxuan.rokid.network.http + +data class ApiResponse( + val code: String, + val message: String, + val data: T +) { + fun isSuccess() = code == "200" +} + +data class Test( + val userId: String, + val name: String +) \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/http/ApiService.kt b/app/src/main/java/com/yuanxuan/rokid/network/http/ApiService.kt new file mode 100644 index 0000000..dcec3a1 --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/http/ApiService.kt @@ -0,0 +1,12 @@ +package com.yuanxuan.rokid.network.http + +import com.yuanxuan.rokid.network.NetUtils +import retrofit2.http.GET +import retrofit2.http.Url + +interface ApiService { + + @GET + suspend fun test(@Url url: String = "${NetUtils.getBaseUrl()}/test"): ApiResponse + +} \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/http/NetworkException.kt b/app/src/main/java/com/yuanxuan/rokid/network/http/NetworkException.kt new file mode 100644 index 0000000..68d6efe --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/http/NetworkException.kt @@ -0,0 +1,5 @@ +package com.yuanxuan.rokid.network.http + +import java.io.IOException + +class NetworkException(message: String, cause: Throwable? = null) : IOException(message, cause) \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/http/OkHttpManager.kt b/app/src/main/java/com/yuanxuan/rokid/network/http/OkHttpManager.kt new file mode 100644 index 0000000..bd4b7f9 --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/http/OkHttpManager.kt @@ -0,0 +1,135 @@ +package com.yuanxuan.rokid.network.http + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.yuanxuan.rokid.network.NetUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import okhttp3.Call +import okhttp3.Callback +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okhttp3.logging.HttpLoggingInterceptor +import timber.log.Timber +import java.io.IOException +import java.lang.Exception +import java.lang.reflect.Type +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.time.Duration.Companion.milliseconds + +class OkHttpManager { + + private val soTimeoutMillis = 30.milliseconds + private val gson by lazy { + Gson() + } + + private val okhttpClient by lazy { + val httpLoggingInterceptor by lazy { + HttpLoggingInterceptor { message -> + Timber.tag("OkHttp").d(message) + }.apply { + level = HttpLoggingInterceptor.Level.BODY + } + } + OkHttpClient.Builder() + .connectTimeout(soTimeoutMillis) + .readTimeout(soTimeoutMillis) + .addInterceptor(httpLoggingInterceptor) + .build() + } + + suspend fun testApi(): Test { + return makeRequest( + path = "", + method = "GET", + ) + } + + private suspend inline fun makeRequest( + path: String, + method: String, + jsonBody: String? = null, + headers: Map = emptyMap(), + ): T = withContext(Dispatchers.IO) { + + val requestBody = jsonBody?.toRequestBody("application/json".toMediaTypeOrNull()) + val request = buildRequest( + path = path, + method = method, + body = requestBody, + headers = headers + ) + try { + val response = okhttpClient.newCall(request).executeAwait() + val responseBody = response.body.string() + //过滤http错误 + if (response.isSuccessful.not()) { + throw NetworkException( + message = "Http error: ${response.code} ${response.message}" + ) + } + val typeToken = object : TypeToken>() {} + val typeOfT: Type = typeToken.type + val apiResponse: ApiResponse = gson.fromJson(responseBody, typeOfT) + if (apiResponse.isSuccess().not()) { + throw NetworkException( + message = "Business error: ${apiResponse.code} ${apiResponse.message}" + ) + } + return@withContext apiResponse.data as T + } catch (e: Exception) { + throw NetworkException("Http error: ${e.message}") + } + } + + private fun buildRequest( + path: String, + method: String, + body: RequestBody?, + headers: Map + ): Request { + val request = Request.Builder() + .url(NetUtils.getBaseUrl() + path) + .method(method, body) + headers.forEach { (key, value) -> + request.addHeader(key, value) + } + return request.build() + } + + private suspend fun Call.executeAwait(): Response = + suspendCancellableCoroutine { continuation -> + enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + if (continuation.context.isActive) { + continuation.resumeWithException(e) + } + } + + override fun onResponse(call: Call, response: Response) { + //协程被取消忽略请求结果 + if (continuation.context.isActive) { + continuation.resume(response) + } + } + }) + + continuation.invokeOnCancellation { + try { + //协程被取消 终端网络请求 + cancel() + } catch (_: Throwable) { + // Ignore + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/http/RequestApi.kt b/app/src/main/java/com/yuanxuan/rokid/network/http/RequestApi.kt new file mode 100644 index 0000000..696a20c --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/http/RequestApi.kt @@ -0,0 +1,13 @@ +package com.yuanxuan.rokid.network.http + +class RequestApi( + private val okHttp: OkHttpManager +) { + + suspend fun testApi(): Result { + return runCatching { + okHttp.testApi() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/http/RetrofitClient.kt b/app/src/main/java/com/yuanxuan/rokid/network/http/RetrofitClient.kt new file mode 100644 index 0000000..5863d12 --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/http/RetrofitClient.kt @@ -0,0 +1,37 @@ +package com.yuanxuan.rokid.network.http + +import com.google.gson.GsonBuilder +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import timber.log.Timber +import kotlin.time.Duration.Companion.seconds + +object RetrofitClient { + private val okHttpClient: OkHttpClient by lazy { + val httpLoggingInterceptor by lazy { + HttpLoggingInterceptor { message -> + Timber.tag("OkHttp").d(message) + }.apply { + level = HttpLoggingInterceptor.Level.BODY + } + } + OkHttpClient.Builder() + .connectTimeout(15.seconds) + .readTimeout(30.seconds) + .writeTimeout(30.seconds) + .addInterceptor(httpLoggingInterceptor) + .build() + } + private val retrofit: Retrofit by lazy { + Retrofit.Builder() + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())) + .build() + } + + val apiService: ApiService by lazy { + retrofit.create(ApiService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/websocket/OkHttpWebSocketConnection.kt b/app/src/main/java/com/yuanxuan/rokid/network/websocket/OkHttpWebSocketConnection.kt new file mode 100644 index 0000000..66928ea --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/websocket/OkHttpWebSocketConnection.kt @@ -0,0 +1,72 @@ +package com.yuanxuan.rokid.network.websocket + +import com.yuanxuan.rokid.network.NetUtils +import com.yuanxuan.rokid.network.websocket.WebSocketConnection.Companion.DEFAULT_SEND_TIMEOUT +import com.yuanxuan.rokid.network.websocket.WebSocketConnection.Companion.PING_INTERVAL_TIME +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okhttp3.logging.HttpLoggingInterceptor +import timber.log.Timber +import java.net.Inet4Address +import java.net.NetworkInterface + +class OkHttpWebSocketConnection() : WebSocketListener(), WebSocketConnection { + + private val _webSocketConnectionStateFlow: MutableStateFlow = + MutableStateFlow(WebSocketConnectionState.DISCONNECTED) + val webSocketConnectionStateFlow = _webSocketConnectionStateFlow.asStateFlow() + + private var client: WebSocket? = null + private val httpLoggingInterceptor by lazy { + HttpLoggingInterceptor { message -> + Timber.tag("OkHttp").d(message) + }.apply { + level = HttpLoggingInterceptor.Level.BODY + } + } + + @Synchronized + override fun connect() { + val okHttpClient = OkHttpClient.Builder() + .readTimeout(DEFAULT_SEND_TIMEOUT) + .pingInterval(PING_INTERVAL_TIME) + .addInterceptor(httpLoggingInterceptor) + .build() + + val request = Request.Builder().url(NetUtils.getBaseUrl()).build() + _webSocketConnectionStateFlow.update { + WebSocketConnectionState.CONNECTING + } + client = okHttpClient.newWebSocket(request, this) + } + + override fun isDead(): Boolean { + TODO("Not yet implemented") + } + + override fun disconnect() { + TODO("Not yet implemented") + } + + override fun sendRequest( + request: WebSocketRequestMessage, + timeoutSeconds: Long + ) { + client?.send(request.requestId) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Timber.e("websocket断开连接 ${t}") + _webSocketConnectionStateFlow.update { WebSocketConnectionState.FAILED } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketConnection.kt b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketConnection.kt new file mode 100644 index 0000000..c294300 --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketConnection.kt @@ -0,0 +1,28 @@ +package com.yuanxuan.rokid.network.websocket + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlin.time.Duration.Companion.seconds + +interface WebSocketConnection { + + companion object { + val DEFAULT_SEND_TIMEOUT = 10.seconds + val PING_INTERVAL_TIME = 30.seconds + } + + fun connect() + + fun isDead(): Boolean + + fun disconnect() + + fun sendRequest(request: WebSocketRequestMessage) { + return sendRequest(request, DEFAULT_SEND_TIMEOUT.inWholeSeconds) + } + + fun sendRequest( + request: WebSocketRequestMessage, + timeoutSeconds: Long + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketConnectionState.java b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketConnectionState.java new file mode 100644 index 0000000..de75e5e --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketConnectionState.java @@ -0,0 +1,15 @@ +package com.yuanxuan.rokid.network.websocket; + +public enum WebSocketConnectionState { + DISCONNECTED, + CONNECTING, + CONNECTED, + DISCONNECTING, + AUTHENTICATION_FAILED, + REMOTE_DEPRECATED, + FAILED; + + public boolean isFailure() { + return this == AUTHENTICATION_FAILED || this == REMOTE_DEPRECATED || this == FAILED; + } +} diff --git a/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketManager.kt b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketManager.kt new file mode 100644 index 0000000..ac11bf0 --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketManager.kt @@ -0,0 +1,51 @@ +package com.yuanxuan.rokid.network.websocket + +import android.content.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.util.Date +import kotlin.time.Duration.Companion.seconds + +class WebSocketManager(context: Context, scope: CoroutineScope) { + + private val webSocketConnection = OkHttpWebSocketConnection() + + init { + scope.launch { + webSocketConnection.webSocketConnectionStateFlow.collectLatest { state -> + when (state) { + WebSocketConnectionState.DISCONNECTED, + WebSocketConnectionState.CONNECTING, + WebSocketConnectionState.CONNECTED, + WebSocketConnectionState.DISCONNECTING, + WebSocketConnectionState.AUTHENTICATION_FAILED, + WebSocketConnectionState.REMOTE_DEPRECATED -> { + } + + WebSocketConnectionState.FAILED -> { + WebSocketReconnectWorker.schedule(context) + } + } + } + } + + scope.launch { + while (true) { + delay(5.seconds.inWholeMilliseconds) + webSocketConnection.sendRequest( + WebSocketRequestMessage( + requestId = "测试数据 ${Date().time}" + ) + ) + } + } + } + + fun connect() { + webSocketConnection.connect() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketReconnectWorker.kt b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketReconnectWorker.kt new file mode 100644 index 0000000..0dc3085 --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketReconnectWorker.kt @@ -0,0 +1,44 @@ +package com.yuanxuan.rokid.network.websocket + +import android.content.Context +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.yuanxuan.rokid.dependencies.AppDependencies +import timber.log.Timber + +class WebSocketReconnectWorker(context: Context, workerParams: WorkerParameters) : Worker( + context, + workerParams +) { + override fun doWork(): Result { + Timber.d("尝试开始重连") + AppDependencies.deviceServiceManager.playTTS("开始重连") + AppDependencies.webSocketManager.connect() + return Result.success() + } + + companion object { + const val WORE_NAME = "WebSocketReconnectWorker" + fun schedule(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val request = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .build() + WorkManager.getInstance(context).enqueueUniqueWork( + uniqueWorkName = WORE_NAME, + existingWorkPolicy = ExistingWorkPolicy.REPLACE, + request = request + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketRequestMessage.kt b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketRequestMessage.kt new file mode 100644 index 0000000..e1755fe --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketRequestMessage.kt @@ -0,0 +1,5 @@ +package com.yuanxuan.rokid.network.websocket + +data class WebSocketRequestMessage( + val requestId: String +) diff --git a/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketResponseMessage.kt b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketResponseMessage.kt new file mode 100644 index 0000000..d325748 --- /dev/null +++ b/app/src/main/java/com/yuanxuan/rokid/network/websocket/WebSocketResponseMessage.kt @@ -0,0 +1,5 @@ +package com.yuanxuan.rokid.network.websocket + +data class WebSocketResponseMessage( + val requestId: String +) diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..e2b20e9 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,7 @@ + + + + 192.168.2.83 + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f3c97ab..c3a5e43 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,9 @@ activity = "1.8.0" constraintlayout = "2.1.4" timber = "5.0.1" okhttp = "5.3.0" +worker = "2.11.0" +retrofit = "3.0.0" +gson = "2.13.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -24,6 +27,10 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const logger-timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } +androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "worker" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } +gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } [plugins]