feat: App 应用内更新
This commit is contained in:
parent
c9a2b78a74
commit
0b0c863765
|
|
@ -23,13 +23,10 @@ misc.xml
|
||||||
deploymentTargetDropDown.xml
|
deploymentTargetDropDown.xml
|
||||||
render.experimental.xml
|
render.experimental.xml
|
||||||
|
|
||||||
# Keystore files
|
|
||||||
*.jks
|
|
||||||
*.keystore
|
|
||||||
|
|
||||||
# Google Services (e.g. APIs or Firebase)
|
# Google Services (e.g. APIs or Firebase)
|
||||||
google-services.json
|
google-services.json
|
||||||
|
|
||||||
# Android Profiling
|
# Android Profiling
|
||||||
*.hprof
|
*.hprof
|
||||||
|
app/release
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val keystores: Map<String, Properties?> =
|
||||||
|
mapOf("release" to loadKeystoreProperties("keystore.properties"))
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.yuanxuan.rokid"
|
namespace = "com.yuanxuan.rokid"
|
||||||
compileSdk {
|
compileSdk {
|
||||||
|
|
@ -19,8 +25,23 @@ android {
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keystores["release"]?.let { proerties ->
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
storeFile = rootProject.file(proerties["storeFile"] as String)
|
||||||
|
storePassword = proerties["storePassword"] as String
|
||||||
|
keyAlias = proerties["keyAlias"] as String
|
||||||
|
keyPassword = proerties["keyPassword"] as String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
|
@ -59,4 +80,16 @@ dependencies {
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadKeystoreProperties(filename: String): Properties? {
|
||||||
|
val keystorePropertiesFile = file("${project.rootDir}/$filename")
|
||||||
|
|
||||||
|
return if (keystorePropertiesFile.exists()) {
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||||
|
keystoreProperties
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".RokidApplication"
|
android:name=".RokidApplication"
|
||||||
|
|
@ -18,8 +19,8 @@
|
||||||
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">
|
android:usesCleartextTraffic="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
@ -30,6 +31,16 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<service android:name="com.yuanxuan.rokid.keeplive.KeepLiveService" />
|
<service android:name="com.yuanxuan.rokid.keeplive.KeepLiveService" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileProvider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/parovider_path" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package com.yuanxuan.rokid
|
package com.yuanxuan.rokid
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.KeyEvent
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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
|
||||||
|
|
@ -44,38 +44,23 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * 触摸板事件
|
* 触摸板事件
|
||||||
// * [KeyEvent.KEYCODE_DPAD_DOWN] 和 [KeyEvent.KEYCODE_DPAD_RIGHT] 同时响应
|
* [KeyEvent.KEYCODE_DPAD_DOWN] 和 [KeyEvent.KEYCODE_DPAD_RIGHT] 同时响应
|
||||||
// * 所以直接消费掉一个
|
* 所以直接消费掉一个
|
||||||
// */
|
*/
|
||||||
// override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
// return if (event.action == KeyEvent.ACTION_DOWN) {
|
return if (AppDependencies.deviceServiceManager.instructState.value ==
|
||||||
// when (event.keyCode) {
|
DeviceServiceManager.InstructState.WaitingInstructState
|
||||||
// KeyEvent.KEYCODE_DPAD_DOWN -> {
|
) {
|
||||||
// true
|
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
// }
|
AppDependencies.deviceServiceManager.quitInstructReceived()
|
||||||
//
|
}
|
||||||
// KeyEvent.KEYCODE_DPAD_UP -> {
|
true
|
||||||
// true
|
} else {
|
||||||
// }
|
super.dispatchKeyEvent(event)
|
||||||
//
|
}
|
||||||
// KeyEvent.KEYCODE_DPAD_LEFT -> {
|
}
|
||||||
// viewModel.onKeyEventDispatched(MainViewModel.KeyEvent.DpadLeft)
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
|
||||||
// viewModel.onKeyEventDispatched(MainViewModel.KeyEvent.DpadRight)
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// else -> super.dispatchKeyEvent(event)
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// super.dispatchKeyEvent(event)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
private fun observer() {
|
private fun observer() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ 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.ApiRepository
|
||||||
import com.yuanxuan.rokid.network.http.ApiService
|
import com.yuanxuan.rokid.network.http.ApiService
|
||||||
|
import com.yuanxuan.rokid.network.http.DownloadApiRepository
|
||||||
|
import com.yuanxuan.rokid.network.http.DownloadRetrofitClient
|
||||||
import com.yuanxuan.rokid.network.http.RetrofitClient
|
import com.yuanxuan.rokid.network.http.RetrofitClient
|
||||||
import com.yuanxuan.rokid.network.websocket.WebSocketManager
|
import com.yuanxuan.rokid.network.websocket.WebSocketManager
|
||||||
import com.yuanxuan.rokid.toast.ToastManager
|
import com.yuanxuan.rokid.toast.ToastManager
|
||||||
|
|
@ -36,15 +38,25 @@ object AppDependencies {
|
||||||
provider.provideWebSocketManager()
|
provider.provideWebSocketManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
val retrofitClient by lazy {
|
private val retrofitClient by lazy {
|
||||||
provider.provideRetrofitClient()
|
provider.provideRetrofitClient()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val downloadRetrofitClient by lazy {
|
||||||
|
provider.provideDownloadRetrofitClient()
|
||||||
|
}
|
||||||
|
|
||||||
val requestApi by lazy {
|
val requestApi by lazy {
|
||||||
provider.provideApiRepository(
|
provider.provideApiRepository(
|
||||||
apiService = retrofitClient.apiService
|
apiService = retrofitClient.apiService
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val downloadApi by lazy {
|
||||||
|
provider.provideDownloadApiRepository(
|
||||||
|
apiService = downloadRetrofitClient.apiService
|
||||||
|
)
|
||||||
|
}
|
||||||
val toastManager by lazy {
|
val toastManager by lazy {
|
||||||
provider.provideToastManager()
|
provider.provideToastManager()
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +65,9 @@ object AppDependencies {
|
||||||
fun provideDeviceServiceManager(): DeviceServiceManager
|
fun provideDeviceServiceManager(): DeviceServiceManager
|
||||||
fun provideWebSocketManager(): WebSocketManager
|
fun provideWebSocketManager(): WebSocketManager
|
||||||
fun provideRetrofitClient(): RetrofitClient
|
fun provideRetrofitClient(): RetrofitClient
|
||||||
|
fun provideDownloadRetrofitClient(): DownloadRetrofitClient
|
||||||
fun provideApiRepository(apiService: ApiService): ApiRepository
|
fun provideApiRepository(apiService: ApiService): ApiRepository
|
||||||
|
fun provideDownloadApiRepository(apiService: ApiService): DownloadApiRepository
|
||||||
fun provideToastManager(): ToastManager
|
fun provideToastManager(): ToastManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ 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.ApiRepository
|
||||||
import com.yuanxuan.rokid.network.http.ApiService
|
import com.yuanxuan.rokid.network.http.ApiService
|
||||||
|
import com.yuanxuan.rokid.network.http.DownloadApiRepository
|
||||||
|
import com.yuanxuan.rokid.network.http.DownloadRetrofitClient
|
||||||
import com.yuanxuan.rokid.network.http.RetrofitClient
|
import com.yuanxuan.rokid.network.http.RetrofitClient
|
||||||
import com.yuanxuan.rokid.network.websocket.WebSocketManager
|
import com.yuanxuan.rokid.network.websocket.WebSocketManager
|
||||||
import com.yuanxuan.rokid.toast.ToastManager
|
import com.yuanxuan.rokid.toast.ToastManager
|
||||||
|
|
@ -26,12 +28,20 @@ class ApplicationDependencyProvider(val context: Application, val scope: Corouti
|
||||||
return RetrofitClient()
|
return RetrofitClient()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun provideDownloadRetrofitClient(): DownloadRetrofitClient {
|
||||||
|
return DownloadRetrofitClient()
|
||||||
|
}
|
||||||
|
|
||||||
override fun provideApiRepository(apiService: ApiService): ApiRepository {
|
override fun provideApiRepository(apiService: ApiService): ApiRepository {
|
||||||
return ApiRepository(
|
return ApiRepository(
|
||||||
apiService = apiService
|
apiService = apiService
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun provideDownloadApiRepository(apiService: ApiService): DownloadApiRepository {
|
||||||
|
return DownloadApiRepository(apiService = apiService)
|
||||||
|
}
|
||||||
|
|
||||||
override fun provideToastManager(): ToastManager {
|
override fun provideToastManager(): ToastManager {
|
||||||
return ToastManager(
|
return ToastManager(
|
||||||
context = context,
|
context = context,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package com.yuanxuan.rokid.extension
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ fun View.fadeOut(duration: Long) {
|
||||||
.setDuration(duration)
|
.setDuration(duration)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
this@fadeOut.isGone = true
|
this@fadeOut.isInvisible = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
package com.yuanxuan.rokid.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.work.BackoffPolicy
|
||||||
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.NetworkType
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
import androidx.work.OutOfQuotaPolicy
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.yuanxuan.rokid.dependencies.AppDependencies
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class ApkDownloadWorker(appContext: Context, params: WorkerParameters) :
|
||||||
|
CoroutineWorker(appContext, params) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val WORE_NAME = "ApkDownloadWorker"
|
||||||
|
fun schedule(context: Context) {
|
||||||
|
val constraints = Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = OneTimeWorkRequestBuilder<ApkDownloadWorker>()
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
|
//重试策略 一共5次 线性增加间隔时长
|
||||||
|
.setBackoffCriteria(
|
||||||
|
backoffPolicy = BackoffPolicy.LINEAR,
|
||||||
|
backoffDelay = 30,
|
||||||
|
timeUnit = TimeUnit.SECONDS
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||||
|
uniqueWorkName = WORE_NAME,
|
||||||
|
existingWorkPolicy = ExistingWorkPolicy.REPLACE,
|
||||||
|
request = request
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
Timber.d("重试次数 $runAttemptCount")
|
||||||
|
// 重试限制5次
|
||||||
|
if (runAttemptCount >= 5) {
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
AppDependencies.downloadApi.apkDownload("http://192.168.2.83:8000/download/app-release.apk")
|
||||||
|
.fold(
|
||||||
|
onSuccess = {
|
||||||
|
installApkSilent(it)
|
||||||
|
return Result.success()
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
return Result.retry()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installApkSilent(apkFile: File) {
|
||||||
|
if (!apkFile.exists()) {
|
||||||
|
Timber.e("APK 文件不存在,无法安装: ${apkFile.absolutePath}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
val uri: Uri = FileProvider.getUriForFile(
|
||||||
|
applicationContext,
|
||||||
|
"${applicationContext.packageName}.fileProvider",
|
||||||
|
apkFile
|
||||||
|
)
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
|
||||||
|
intent.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
|
try {
|
||||||
|
applicationContext.startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "启动安装程序失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,8 @@ import java.net.NetworkInterface
|
||||||
|
|
||||||
object NetUtils {
|
object NetUtils {
|
||||||
|
|
||||||
fun getBaseUrl() = "http://${getLocalIPV4address()}.83:8765"
|
fun getBaseUrl() = "http://${getLocalIPV4address()}.70:7070"
|
||||||
|
// fun getBaseUrl() = "http://${getLocalIPV4address()}.83:8765"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ package com.yuanxuan.rokid.network.http
|
||||||
import com.yuanxuan.rokid.network.NetUtils
|
import com.yuanxuan.rokid.network.NetUtils
|
||||||
import com.yuanxuan.rokid.network.bean.ApiResponse
|
import com.yuanxuan.rokid.network.bean.ApiResponse
|
||||||
import com.yuanxuan.rokid.network.bean.Test
|
import com.yuanxuan.rokid.network.bean.Test
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import retrofit2.Response
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Streaming
|
||||||
import retrofit2.http.Url
|
import retrofit2.http.Url
|
||||||
|
|
||||||
interface ApiService {
|
interface ApiService {
|
||||||
|
|
@ -11,4 +14,8 @@ interface ApiService {
|
||||||
@GET
|
@GET
|
||||||
suspend fun test(@Url url: String = "${NetUtils.getBaseUrl()}/test"): ApiResponse<Test>
|
suspend fun test(@Url url: String = "${NetUtils.getBaseUrl()}/test"): ApiResponse<Test>
|
||||||
|
|
||||||
|
@Streaming
|
||||||
|
@GET
|
||||||
|
suspend fun downloadFiles(@Url fileUrl: String): Response<ResponseBody>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.yuanxuan.rokid.network.http
|
||||||
|
|
||||||
|
import com.yuanxuan.rokid.dependencies.AppDependencies
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
class DownloadApiRepository(private val apiService: ApiService) {
|
||||||
|
|
||||||
|
suspend fun apkDownload(url: String): Result<File> = withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val response = apiService.downloadFiles(url)
|
||||||
|
if (!response.isSuccessful) throw HttpException(response)
|
||||||
|
val body = response.body() ?: throw IOException("下载失败:响应成功但响应体为空。")
|
||||||
|
val file = File(AppDependencies.application.externalCacheDir, "rokid.apk")
|
||||||
|
val isSuccess = writeResponseBodyToDisk(body, file)
|
||||||
|
|
||||||
|
if (!isSuccess) {
|
||||||
|
throw IOException("将文件写入磁盘时失败。")
|
||||||
|
}
|
||||||
|
file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeResponseBodyToDisk(body: ResponseBody, file: File): Boolean {
|
||||||
|
var inputStream: InputStream? = null
|
||||||
|
var outputStream: OutputStream? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
val fileReader = ByteArray(102400)
|
||||||
|
val fileSize = body.contentLength()
|
||||||
|
var fileSizeDownloaded: Long = 0
|
||||||
|
|
||||||
|
inputStream = body.byteStream()
|
||||||
|
outputStream = FileOutputStream(file)
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val read = inputStream.read(fileReader)
|
||||||
|
if (read == -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
outputStream.write(fileReader, 0, read)
|
||||||
|
fileSizeDownloaded += read.toLong()
|
||||||
|
|
||||||
|
val progress = (fileSizeDownloaded * 100 / fileSize).toInt()
|
||||||
|
Timber.d("文件下载中... $progress%")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputStream.flush()
|
||||||
|
Timber.d("文件下载成功: ${file.path}")
|
||||||
|
return true
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e, "写入文件时发生 IO 异常")
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
inputStream?.close()
|
||||||
|
outputStream?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
class DownloadRetrofitClient {
|
||||||
|
private val okHttpClient: OkHttpClient by lazy {
|
||||||
|
val httpLoggingInterceptor by lazy {
|
||||||
|
HttpLoggingInterceptor { message ->
|
||||||
|
Timber.tag("OkHttp").d(message)
|
||||||
|
}.apply {
|
||||||
|
level = HttpLoggingInterceptor.Level.BASIC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.connectTimeout(15.seconds)
|
||||||
|
.readTimeout(30.seconds)
|
||||||
|
.writeTimeout(30.seconds)
|
||||||
|
.addInterceptor(httpLoggingInterceptor)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
private val retrofit: Retrofit by lazy {
|
||||||
|
Retrofit.Builder()
|
||||||
|
.client(okHttpClient)
|
||||||
|
.baseUrl("http://localhost/")
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val apiService: ApiService by lazy {
|
||||||
|
retrofit.create(ApiService::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ class RetrofitClient {
|
||||||
private val retrofit: Retrofit by lazy {
|
private val retrofit: Retrofit by lazy {
|
||||||
Retrofit.Builder()
|
Retrofit.Builder()
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
|
.baseUrl("http://localhost/")
|
||||||
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
|
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,7 @@ import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.NetworkInterface
|
|
||||||
|
|
||||||
class OkHttpWebSocketConnection() : WebSocketListener(), WebSocketConnection {
|
class OkHttpWebSocketConnection() : WebSocketListener(), WebSocketConnection {
|
||||||
|
|
||||||
|
|
@ -23,20 +20,12 @@ class OkHttpWebSocketConnection() : WebSocketListener(), WebSocketConnection {
|
||||||
val webSocketConnectionStateFlow = _webSocketConnectionStateFlow.asStateFlow()
|
val webSocketConnectionStateFlow = _webSocketConnectionStateFlow.asStateFlow()
|
||||||
|
|
||||||
private var client: WebSocket? = null
|
private var client: WebSocket? = null
|
||||||
private val httpLoggingInterceptor by lazy {
|
|
||||||
HttpLoggingInterceptor { message ->
|
|
||||||
Timber.tag("OkHttp").d(message)
|
|
||||||
}.apply {
|
|
||||||
level = HttpLoggingInterceptor.Level.BODY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun connect() {
|
override fun connect() {
|
||||||
val okHttpClient = OkHttpClient.Builder()
|
val okHttpClient = OkHttpClient.Builder()
|
||||||
.readTimeout(DEFAULT_SEND_TIMEOUT)
|
.readTimeout(DEFAULT_SEND_TIMEOUT)
|
||||||
.pingInterval(PING_INTERVAL_TIME)
|
.pingInterval(PING_INTERVAL_TIME)
|
||||||
.addInterceptor(httpLoggingInterceptor)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = Request.Builder().url(NetUtils.getBaseUrl()).build()
|
val request = Request.Builder().url(NetUtils.getBaseUrl()).build()
|
||||||
|
|
@ -62,6 +51,11 @@ class OkHttpWebSocketConnection() : WebSocketListener(), WebSocketConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
|
Timber.d(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
|
Timber.d("websocket onOpen")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@
|
||||||
android:id="@+id/nav_host_fragment"
|
android:id="@+id/nav_host_fragment"
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/lottie_voice_input"
|
||||||
app:defaultNavHost="true"
|
app:defaultNavHost="true"
|
||||||
app:navGraph="@navigation/app_navigation" />
|
app:navGraph="@navigation/app_navigation" />
|
||||||
|
|
||||||
|
|
@ -57,14 +59,15 @@
|
||||||
|
|
||||||
<com.airbnb.lottie.LottieAnimationView
|
<com.airbnb.lottie.LottieAnimationView
|
||||||
android:id="@+id/lottie_voice_input"
|
android:id="@+id/lottie_voice_input"
|
||||||
android:layout_width="200dp"
|
android:layout_width="150dp"
|
||||||
android:layout_height="100dp"
|
android:layout_height="50dp"
|
||||||
android:visibility="gone"
|
android:visibility="invisible"
|
||||||
app:layout_constraintBottom_toTopOf="@id/bottom"
|
app:layout_constraintBottom_toTopOf="@id/bottom"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:lottie_autoPlay="false"
|
||||||
app:lottie_loop="true"
|
app:lottie_loop="true"
|
||||||
app:lottie_rawRes="@raw/lottie_input_voice"
|
app:lottie_rawRes="@raw/voice_white"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
|
|
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-cache-path
|
||||||
|
name="external_cache_files"
|
||||||
|
path="." />
|
||||||
|
</paths>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
storeFile=keystore/release_keystore.jks
|
||||||
|
storePassword=S<+*T;KcbR?yMm3b
|
||||||
|
keyAlias=key0
|
||||||
|
keyPassword=]+*7;Vn+zpA=r{KX
|
||||||
Loading…
Reference in New Issue