add basic updater implementation in gigawear
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/bin
|
||||
@@ -0,0 +1,14 @@
|
||||
plugins {
|
||||
alias(libs.plugins.convention.jvm)
|
||||
alias(libs.plugins.convention.ktor.client)
|
||||
alias(libs.plugins.convention.koin)
|
||||
alias(libs.plugins.convention.serialization)
|
||||
alias(libs.plugins.ktor)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.ktor.server.core.jvm)
|
||||
implementation(project(":libs:backend-base"))
|
||||
implementation(project(":libs:http-client"))
|
||||
implementation(project(":feature:updater:updater-common"))
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package ru.shadowsparky.updater.backend
|
||||
|
||||
import org.koin.core.annotation.ComponentScan
|
||||
import org.koin.core.annotation.Configuration
|
||||
import org.koin.core.annotation.Module
|
||||
|
||||
@Module
|
||||
@ComponentScan
|
||||
@Configuration
|
||||
class BackendUpdaterModule
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package ru.shadowsparky.updater.backend.data
|
||||
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.util.cio.readChannel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.backend.data.EnvFetcher
|
||||
import ru.shadowsparky.http.domain.HttpException
|
||||
import ru.shadowsparky.updater.domain.UpdateFetcher
|
||||
import ru.shadowsparky.updater.domain.UpdateInfo
|
||||
import java.io.File
|
||||
|
||||
@Factory
|
||||
class BackendUpdateFetcherFactory(
|
||||
private val envFetcher: EnvFetcher,
|
||||
private val json: Json
|
||||
) {
|
||||
fun create(userId: Long): BackendUpdateFetcher {
|
||||
return BackendUpdateFetcher(envFetcher, json, userId)
|
||||
}
|
||||
}
|
||||
|
||||
class BackendUpdateFetcher(
|
||||
private val envFetcher: EnvFetcher,
|
||||
private val json: Json,
|
||||
private val userId: Long
|
||||
) : UpdateFetcher {
|
||||
private val fetchMutex = Mutex()
|
||||
private val updateFileMutex = Mutex()
|
||||
|
||||
override suspend fun fetch(versionCode: Long): UpdateInfo {
|
||||
updateInfo?.let { return it }
|
||||
fetchMutex.withLock {
|
||||
updateInfo?.let { return it }
|
||||
return fetchInternal().apply { updateInfo = this }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchInternal(): UpdateInfo = withContext(Dispatchers.IO) {
|
||||
val configPath = envFetcher.get("UPDATE_CONFIG")
|
||||
if (configPath.isBlank()) {
|
||||
UpdateInfo()
|
||||
} else {
|
||||
json.decodeFromString<UpdateInfo>(File(configPath).readText())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateFile(): File {
|
||||
updateFile?.let { return it }
|
||||
updateFileMutex.withLock {
|
||||
updateFile?.let { return it }
|
||||
return updateFileInternal().apply { updateFile = this }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateFileInternal(): File = withContext(Dispatchers.IO) {
|
||||
val updateFilePath = envFetcher.get("UPDATE_FILE")
|
||||
if (updateFilePath.isBlank()) throw HttpException(HttpStatusCode.NotFound.value, "Update not found")
|
||||
val file = File(updateFilePath)
|
||||
file.readChannel()
|
||||
if (!file.exists()) throw HttpException(HttpStatusCode.NotFound.value, "Update not exists")
|
||||
file
|
||||
}
|
||||
|
||||
private companion object {
|
||||
var updateInfo: UpdateInfo? = null
|
||||
var updateFile: File? = null
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package ru.shadowsparky.updater.backend.presentation
|
||||
|
||||
import io.ktor.server.application.ApplicationCall
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondFile
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.util.getOrFail
|
||||
import ru.shadowsparky.backend.presentation.obtainUserId
|
||||
import ru.shadowsparky.updater.backend.data.BackendUpdateFetcherFactory
|
||||
import ru.shadowsparky.updater.domain.UpdateFetcher
|
||||
|
||||
fun Route.setupUpdates(
|
||||
updateFetcherFactory: BackendUpdateFetcherFactory,
|
||||
prefix: String = "",
|
||||
onUserId: suspend ApplicationCall.() -> Long = { obtainUserId() }
|
||||
) {
|
||||
get("${prefix}checkUpdates") {
|
||||
val versionCode = call.parameters.getOrFail(UpdateFetcher.ARG_VERSION_CODE).toLong()
|
||||
val updateFetcher = updateFetcherFactory.create(onUserId(call))
|
||||
call.respond(updateFetcher.fetch(versionCode))
|
||||
}
|
||||
get("${prefix}getApk") {
|
||||
val updateFetcher = updateFetcherFactory.create(onUserId(call))
|
||||
call.respondFile(updateFetcher.updateFile())
|
||||
}
|
||||
}
|
||||
+32
-36
@@ -20,6 +20,8 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
@@ -42,7 +44,6 @@ import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import ru.shadowsparky.ui.components.AlertContent
|
||||
import ru.shadowsparky.ui.components.ErrorContent
|
||||
import ru.shadowsparky.ui.components.LazyColumn
|
||||
import ru.shadowsparky.ui.components.Loading
|
||||
import ru.shadowsparky.updater.domain.UpdateState
|
||||
|
||||
@@ -128,8 +129,9 @@ private fun UpdateAvailableContent(
|
||||
focusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
@@ -137,36 +139,26 @@ private fun UpdateAvailableContent(
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(Res.string.updater_changelog_title),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
text = changeLog ?: stringResource(Res.string.updater_changelog_empty),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
item {
|
||||
Button(
|
||||
onClick = onDownloadClick,
|
||||
modifier = Modifier.focusRequester(focusRequester).fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.updater_btn_download),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = stringResource(Res.string.updater_changelog_title),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = changeLog ?: stringResource(Res.string.updater_changelog_empty),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Button(
|
||||
onClick = onDownloadClick,
|
||||
modifier = Modifier.focusRequester(focusRequester).fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(Res.string.updater_btn_download),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,8 +169,9 @@ private fun UpdatePermissionContent(
|
||||
focusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
@@ -210,8 +203,9 @@ private fun UpdateDownloadingContent(
|
||||
totalBytes: Long,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
@@ -241,8 +235,9 @@ private fun UpdateReadyContent(
|
||||
focusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
@@ -269,8 +264,9 @@ private fun UpdateFinishedContent(
|
||||
focusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user