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]