feat: 网络
This commit is contained in:
parent
22ff4384ca
commit
570de3c4e7
|
|
@ -45,8 +45,11 @@ dependencies {
|
||||||
implementation(libs.androidx.activity)
|
implementation(libs.androidx.activity)
|
||||||
implementation(libs.androidx.constraintlayout)
|
implementation(libs.androidx.constraintlayout)
|
||||||
implementation(libs.logger.timber)
|
implementation(libs.logger.timber)
|
||||||
implementation(libs.okhttp)
|
|
||||||
implementation(libs.okhttp.logging.interceptor)
|
implementation(libs.okhttp.logging.interceptor)
|
||||||
|
implementation(libs.androidx.work.ktx)
|
||||||
|
implementation(libs.retrofit)
|
||||||
|
implementation(libs.retrofit.converter.gson)
|
||||||
|
// implementation(libs.gson)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".RokidApplication"
|
android:name=".RokidApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|
@ -11,6 +17,7 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:theme="@style/Theme.Rokid">
|
android:theme="@style/Theme.Rokid">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|
@ -21,6 +28,7 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<service android:name="com.yuanxuan.rokid.keeplive.KeepLiveService" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -7,9 +7,20 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
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.delay
|
||||||
import kotlinx.coroutines.launch
|
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() {
|
class MainActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
@ -21,9 +32,26 @@ class MainActivity : AppCompatActivity() {
|
||||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||||
insets
|
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<Test>(json)
|
||||||
|
|
||||||
|
// onBackPressedDispatcher.addCallback {
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> test(responseBody: String): T {
|
||||||
|
val typeToken = object : TypeToken<ApiResponse<T>>() {}
|
||||||
|
val typeOfT: Type = typeToken.type
|
||||||
|
val apiResponse: ApiResponse<T> = Gson().fromJson(responseBody, typeOfT)
|
||||||
|
return apiResponse.data
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,13 +18,18 @@ class RokidApplication : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
AppDependencies.init(this, ApplicationDependencyProvider(this))
|
AppDependencies.init(this, ApplicationDependencyProvider(this, applicationScope))
|
||||||
/**
|
/**
|
||||||
* 启动APP必须先获取到SN 后面网络依赖
|
* 启动APP必须先获取到SN 后面网络依赖
|
||||||
*/
|
*/
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
val sn = AppDependencies.deviceServiceManager.getSn()
|
runCatching {
|
||||||
Timber.d("sn = $sn")
|
AppDependencies.deviceServiceManager.getSn()
|
||||||
|
}.onSuccess {
|
||||||
|
AppDependencies.webSocketManager.connect()
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 这个服务保证心跳包准时发
|
* 这个服务保证心跳包准时发
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@ package com.yuanxuan.rokid.dependencies
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.yuanxuan.rokid.device.DeviceServiceManager
|
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 _application: Application
|
||||||
private lateinit var provider: Provider
|
private lateinit var provider: Provider
|
||||||
|
|
||||||
|
val application: Application
|
||||||
|
get() = _application
|
||||||
|
|
||||||
fun init(application: Application, provider: Provider) {
|
fun init(application: Application, provider: Provider) {
|
||||||
if (this::_application.isInitialized || this::provider.isInitialized) {
|
if (this::_application.isInitialized || this::provider.isInitialized) {
|
||||||
return
|
return
|
||||||
|
|
@ -24,8 +33,20 @@ object AppDependencies {
|
||||||
provider.provideDeviceServiceManager()
|
provider.provideDeviceServiceManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val webSocketManager by lazy {
|
||||||
|
provider.provideWebSocketManager()
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestApi by lazy {
|
||||||
|
provider.provideApiRepository(
|
||||||
|
apiService = RetrofitClient.apiService
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface Provider {
|
interface Provider {
|
||||||
fun provideDeviceServiceManager(): DeviceServiceManager
|
fun provideDeviceServiceManager(): DeviceServiceManager
|
||||||
|
fun provideWebSocketManager(): WebSocketManager
|
||||||
|
fun provideApiRepository(apiService: ApiService): ApiRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,9 +2,30 @@ package com.yuanxuan.rokid.dependencies
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.yuanxuan.rokid.device.DeviceServiceManager
|
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 {
|
override fun provideDeviceServiceManager(): DeviceServiceManager {
|
||||||
return DeviceServiceManager(context)
|
return DeviceServiceManager(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun provideWebSocketManager(): WebSocketManager {
|
||||||
|
return WebSocketManager(
|
||||||
|
context = context,
|
||||||
|
scope = scope
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun provideApiRepository(apiService: ApiService): ApiRepository {
|
||||||
|
return ApiRepository(
|
||||||
|
apiService = apiService
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
|
|
||||||
class DeviceServiceManager(val context: Application) {
|
class DeviceServiceManager(val context: Application) {
|
||||||
|
|
@ -157,7 +158,12 @@ class DeviceServiceManager(val context: Application) {
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSn() = suspendCancellableCoroutine { continuation ->
|
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
|
sn = service.sn
|
||||||
continuation.resume(sn)
|
continuation.resume(sn)
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +193,6 @@ class DeviceServiceManager(val context: Application) {
|
||||||
}
|
}
|
||||||
systemFuncService = service
|
systemFuncService = service
|
||||||
todo.invoke(service)
|
todo.invoke(service)
|
||||||
Timber.d(" ${service.sn}")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
todo.invoke(systemFuncService!!)
|
todo.invoke(systemFuncService!!)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.yuanxuan.rokid.network.http
|
||||||
|
|
||||||
|
class ApiRepository(private val apiService: ApiService) {
|
||||||
|
|
||||||
|
suspend fun test(): Test? {
|
||||||
|
apiService.test()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.yuanxuan.rokid.network.http
|
||||||
|
|
||||||
|
data class ApiResponse<T>(
|
||||||
|
val code: String,
|
||||||
|
val message: String,
|
||||||
|
val data: T
|
||||||
|
) {
|
||||||
|
fun isSuccess() = code == "200"
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Test(
|
||||||
|
val userId: String,
|
||||||
|
val name: String
|
||||||
|
)
|
||||||
|
|
@ -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<Test>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.yuanxuan.rokid.network.http
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class NetworkException(message: String, cause: Throwable? = null) : IOException(message, cause)
|
||||||
|
|
@ -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<Test>(
|
||||||
|
path = "",
|
||||||
|
method = "GET",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <reified T> makeRequest(
|
||||||
|
path: String,
|
||||||
|
method: String,
|
||||||
|
jsonBody: String? = null,
|
||||||
|
headers: Map<String, String> = 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<ApiResponse<T>>() {}
|
||||||
|
val typeOfT: Type = typeToken.type
|
||||||
|
val apiResponse: ApiResponse<Test> = 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<String, String>
|
||||||
|
): 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.yuanxuan.rokid.network.http
|
||||||
|
|
||||||
|
class RequestApi(
|
||||||
|
private val okHttp: OkHttpManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun testApi(): Result<Test> {
|
||||||
|
return runCatching {
|
||||||
|
okHttp.testApi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<WebSocketConnectionState> =
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<WebSocketReconnectWorker>()
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||||
|
uniqueWorkName = WORE_NAME,
|
||||||
|
existingWorkPolicy = ExistingWorkPolicy.REPLACE,
|
||||||
|
request = request
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.yuanxuan.rokid.network.websocket
|
||||||
|
|
||||||
|
data class WebSocketRequestMessage(
|
||||||
|
val requestId: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.yuanxuan.rokid.network.websocket
|
||||||
|
|
||||||
|
data class WebSocketResponseMessage(
|
||||||
|
val requestId: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<domain includeSubdomains="true">192.168.2.83</domain>
|
||||||
|
</domain-config>
|
||||||
|
</network-security-config>
|
||||||
|
|
||||||
|
|
@ -11,6 +11,9 @@ activity = "1.8.0"
|
||||||
constraintlayout = "2.1.4"
|
constraintlayout = "2.1.4"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
okhttp = "5.3.0"
|
okhttp = "5.3.0"
|
||||||
|
worker = "2.11.0"
|
||||||
|
retrofit = "3.0.0"
|
||||||
|
gson = "2.13.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
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" }
|
logger-timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
|
||||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||||
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", 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]
|
[plugins]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue