feat: App 应用内更新
This commit is contained in:
parent
c9a2b78a74
commit
0b0c863765
|
|
@ -23,13 +23,10 @@ misc.xml
|
|||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
app/release
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
val keystores: Map<String, Properties?> =
|
||||
mapOf("release" to loadKeystoreProperties("keystore.properties"))
|
||||
|
||||
android {
|
||||
namespace = "com.yuanxuan.rokid"
|
||||
compileSdk {
|
||||
|
|
@ -19,8 +25,23 @@ android {
|
|||
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 {
|
||||
debug {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
|
|
@ -59,4 +80,16 @@ dependencies {
|
|||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
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_REMOTE_MESSAGING" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".RokidApplication"
|
||||
|
|
@ -18,8 +19,8 @@
|
|||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@style/Theme.Rokid">
|
||||
android:theme="@style/Theme.Rokid"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
|
@ -30,6 +31,16 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<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>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package com.yuanxuan.rokid
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
|
@ -44,38 +44,23 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 触摸板事件
|
||||
// * [KeyEvent.KEYCODE_DPAD_DOWN] 和 [KeyEvent.KEYCODE_DPAD_RIGHT] 同时响应
|
||||
// * 所以直接消费掉一个
|
||||
// */
|
||||
// override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
// return if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
// when (event.keyCode) {
|
||||
// KeyEvent.KEYCODE_DPAD_DOWN -> {
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// KeyEvent.KEYCODE_DPAD_UP -> {
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* 触摸板事件
|
||||
* [KeyEvent.KEYCODE_DPAD_DOWN] 和 [KeyEvent.KEYCODE_DPAD_RIGHT] 同时响应
|
||||
* 所以直接消费掉一个
|
||||
*/
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
return if (AppDependencies.deviceServiceManager.instructState.value ==
|
||||
DeviceServiceManager.InstructState.WaitingInstructState
|
||||
) {
|
||||
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
AppDependencies.deviceServiceManager.quitInstructReceived()
|
||||
}
|
||||
true
|
||||
} else {
|
||||
super.dispatchKeyEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observer() {
|
||||
lifecycleScope.launch {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ 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.DownloadApiRepository
|
||||
import com.yuanxuan.rokid.network.http.DownloadRetrofitClient
|
||||
import com.yuanxuan.rokid.network.http.RetrofitClient
|
||||
import com.yuanxuan.rokid.network.websocket.WebSocketManager
|
||||
import com.yuanxuan.rokid.toast.ToastManager
|
||||
|
|
@ -36,15 +38,25 @@ object AppDependencies {
|
|||
provider.provideWebSocketManager()
|
||||
}
|
||||
|
||||
val retrofitClient by lazy {
|
||||
private val retrofitClient by lazy {
|
||||
provider.provideRetrofitClient()
|
||||
}
|
||||
|
||||
private val downloadRetrofitClient by lazy {
|
||||
provider.provideDownloadRetrofitClient()
|
||||
}
|
||||
|
||||
val requestApi by lazy {
|
||||
provider.provideApiRepository(
|
||||
apiService = retrofitClient.apiService
|
||||
)
|
||||
}
|
||||
|
||||
val downloadApi by lazy {
|
||||
provider.provideDownloadApiRepository(
|
||||
apiService = downloadRetrofitClient.apiService
|
||||
)
|
||||
}
|
||||
val toastManager by lazy {
|
||||
provider.provideToastManager()
|
||||
}
|
||||
|
|
@ -53,7 +65,9 @@ object AppDependencies {
|
|||
fun provideDeviceServiceManager(): DeviceServiceManager
|
||||
fun provideWebSocketManager(): WebSocketManager
|
||||
fun provideRetrofitClient(): RetrofitClient
|
||||
fun provideDownloadRetrofitClient(): DownloadRetrofitClient
|
||||
fun provideApiRepository(apiService: ApiService): ApiRepository
|
||||
fun provideDownloadApiRepository(apiService: ApiService): DownloadApiRepository
|
||||
fun provideToastManager(): ToastManager
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ 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.DownloadApiRepository
|
||||
import com.yuanxuan.rokid.network.http.DownloadRetrofitClient
|
||||
import com.yuanxuan.rokid.network.http.RetrofitClient
|
||||
import com.yuanxuan.rokid.network.websocket.WebSocketManager
|
||||
import com.yuanxuan.rokid.toast.ToastManager
|
||||
|
|
@ -26,12 +28,20 @@ class ApplicationDependencyProvider(val context: Application, val scope: Corouti
|
|||
return RetrofitClient()
|
||||
}
|
||||
|
||||
override fun provideDownloadRetrofitClient(): DownloadRetrofitClient {
|
||||
return DownloadRetrofitClient()
|
||||
}
|
||||
|
||||
override fun provideApiRepository(apiService: ApiService): ApiRepository {
|
||||
return ApiRepository(
|
||||
apiService = apiService
|
||||
)
|
||||
}
|
||||
|
||||
override fun provideDownloadApiRepository(apiService: ApiService): DownloadApiRepository {
|
||||
return DownloadApiRepository(apiService = apiService)
|
||||
}
|
||||
|
||||
override fun provideToastManager(): ToastManager {
|
||||
return ToastManager(
|
||||
context = context,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.yuanxuan.rokid.extension
|
|||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.view.View
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ fun View.fadeOut(duration: Long) {
|
|||
.setDuration(duration)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
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 {
|
||||
|
||||
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.bean.ApiResponse
|
||||
import com.yuanxuan.rokid.network.bean.Test
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Streaming
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface ApiService {
|
||||
|
|
@ -11,4 +14,8 @@ interface ApiService {
|
|||
@GET
|
||||
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 {
|
||||
Retrofit.Builder()
|
||||
.client(okHttpClient)
|
||||
.baseUrl("http://localhost/")
|
||||
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
|
||||
.build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,7 @@ 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 {
|
||||
|
||||
|
|
@ -23,20 +20,12 @@ class OkHttpWebSocketConnection() : WebSocketListener(), WebSocketConnection {
|
|||
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()
|
||||
|
|
@ -62,6 +51,11 @@ class OkHttpWebSocketConnection() : WebSocketListener(), WebSocketConnection {
|
|||
}
|
||||
|
||||
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?) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@
|
|||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
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:navGraph="@navigation/app_navigation" />
|
||||
|
||||
|
|
@ -57,14 +59,15 @@
|
|||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottie_voice_input"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="100dp"
|
||||
android:visibility="gone"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="50dp"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:lottie_autoPlay="false"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_rawRes="@raw/lottie_input_voice"
|
||||
app:lottie_rawRes="@raw/voice_white"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<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