diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/BackendUpdateFetcher.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/BackendUpdateFetcher.kt
index bf49363..bae095a 100644
--- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/BackendUpdateFetcher.kt
+++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/BackendUpdateFetcher.kt
@@ -9,13 +9,14 @@ import ru.shadowsparky.updater.domain.UpdateFetcher
import ru.shadowsparky.updater.domain.UpdateInfo
import java.io.File
-@Factory
+@Factory(binds = [BackendUpdateFetcher::class])
class BackendUpdateFetcher(
private val envFetcher: EnvFetcher,
private val json: Json,
private val dispatcherProvider: DispatcherProvider
) : UpdateFetcher {
private var config: UpdateInfo? = null
+ private var updateFile: File? = null
override suspend fun fetch(versionCode: Long): UpdateInfo = withContext(dispatcherProvider.io) {
config?.let { return@withContext it }
@@ -28,4 +29,13 @@ class BackendUpdateFetcher(
}
}
}
+
+ suspend fun updateFile(): File = withContext(dispatcherProvider.io) {
+ val updateFilePath = envFetcher.get("UPDATE_FILE")
+ if (updateFilePath.isBlank()) error("Update not found")
+ val file = File(updateFilePath)
+ if (!file.exists()) error("File not exists")
+ updateFile = file
+ file
+ }
}
diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/di/EntryPoint.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/di/EntryPoint.kt
index 26afab3..692f93c 100644
--- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/di/EntryPoint.kt
+++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/di/EntryPoint.kt
@@ -25,7 +25,7 @@ class RoutingEntryPoint(
val savedMovieFactory: SavedMovieRepositoryFactory,
val movieTagFactory: MovieTagRepositoryFactory,
val userTagFactory: UserTagRepositoryFactory,
- val updateFetcher: UpdateFetcher
+ val updateFetcher: BackendUpdateFetcher
)
@Single
diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/UpdateRouting.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/UpdateRouting.kt
index 9715049..f9a2781 100644
--- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/UpdateRouting.kt
+++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/UpdateRouting.kt
@@ -1,15 +1,20 @@
package ru.shadowsparky.vbox.backend.presentation.routing
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.updater.domain.UpdateFetcher
+import ru.shadowsparky.vbox.backend.data.BackendUpdateFetcher
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
-fun Route.setupUpdates(updateFetcher: UpdateFetcher) {
+fun Route.setupUpdates(updateFetcher: BackendUpdateFetcher) {
get("$DYNAMIC_PREFIX/checkUpdates") {
val versionCode = call.parameters.getOrFail(UpdateFetcher.ARG_VERSION_CODE).toLong()
call.respond(updateFetcher.fetch(versionCode))
}
+ get("$DYNAMIC_PREFIX/getApk") {
+ call.respondFile(updateFetcher.updateFile())
+ }
}
diff --git a/libs/updater/updater-client/src/androidMain/AndroidManifest.xml b/libs/updater/updater-client/src/androidMain/AndroidManifest.xml
index 97217f0..107e904 100644
--- a/libs/updater/updater-client/src/androidMain/AndroidManifest.xml
+++ b/libs/updater/updater-client/src/androidMain/AndroidManifest.xml
@@ -2,4 +2,17 @@
xmlns:android="http://schemas.android.com/apk/res/android">
+
+
+
+
+
+
+
diff --git a/libs/updater/updater-client/src/androidMain/kotlin/ru/shadowsparky/updater/data/KtorUpdateDownloader.kt b/libs/updater/updater-client/src/androidMain/kotlin/ru/shadowsparky/updater/data/KtorUpdateDownloader.kt
index bfab356..9887798 100644
--- a/libs/updater/updater-client/src/androidMain/kotlin/ru/shadowsparky/updater/data/KtorUpdateDownloader.kt
+++ b/libs/updater/updater-client/src/androidMain/kotlin/ru/shadowsparky/updater/data/KtorUpdateDownloader.kt
@@ -2,14 +2,17 @@ package ru.shadowsparky.updater.data
import android.app.Application
import io.ktor.client.HttpClient
-import io.ktor.client.request.get
+import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.contentLength
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.readAvailable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.withContext
import org.koin.core.annotation.Factory
+import ru.shadowsparky.domain.DispatcherProvider
import ru.shadowsparky.updater.domain.DownloadProgress
import ru.shadowsparky.updater.domain.UpdateDownloader
import java.io.File
@@ -18,55 +21,58 @@ import java.io.File
class KtorUpdateDownloader(
private val httpClient: HttpClient,
app: Application,
+ private val dispatcherProvider: DispatcherProvider
) : UpdateDownloader {
private val destinationPath = "${app.cacheDir.absolutePath}/update"
override fun download(url: String): Flow = flow {
try {
- val response = httpClient.get(url)
- val contentLength = response.contentLength() ?: -1L
- val channel: ByteReadChannel = response.bodyAsChannel()
+ httpClient.prepareGet(url).execute { response ->
+ val contentLength = response.contentLength() ?: -1L
+ val channel: ByteReadChannel = response.bodyAsChannel()
+ val directory = File(destinationPath)
+ if (!directory.exists()) {
+ directory.mkdirs()
+ }
+ val fileName = "update.apk"
+ val fullPath = "$destinationPath/$fileName"
+ val file = File(fullPath)
- val directory = File(destinationPath)
- if (!directory.exists()) {
- directory.mkdirs()
- }
+ if (file.exists()) file.delete()
- val fileName = "update.apk"
- val fullPath = "$destinationPath/$fileName"
- val file = File(fullPath)
+ val bufferSize = 8192
+ val buffer = ByteArray(bufferSize)
+ var downloadedBytes = 0L
- if (file.exists()) file.delete()
+ file.outputStream().use { outputStream ->
+ while (!channel.isClosedForRead) {
+ val read = channel.readAvailable(buffer, 0, buffer.size)
+ if (read <= 0) break
- val bufferSize = 8192
- val buffer = ByteArray(bufferSize)
- var downloadedBytes = 0L
+ outputStream.write(buffer, 0, read)
+ downloadedBytes += read
- file.outputStream().use { outputStream ->
- while (!channel.isClosedForRead) {
- val read = channel.readAvailable(buffer, 0, buffer.size)
- if (read <= 0) break
-
- outputStream.write(buffer, 0, read)
- downloadedBytes += read
-
- if (contentLength > 0) {
- val progress = downloadedBytes.toFloat() / contentLength.toFloat()
- emit(
- DownloadProgress.Active(
- progress = progress,
- downloadedBytes = downloadedBytes,
- totalBytes = contentLength
+ if (contentLength > 0) {
+ val progress = downloadedBytes.toFloat() / contentLength.toFloat()
+ emit(
+ DownloadProgress.Active(
+ progress = progress,
+ downloadedBytes = downloadedBytes,
+ totalBytes = contentLength
+ )
)
- )
+ }
}
}
}
-
- emit(DownloadProgress.Success(path = file.absolutePath))
+ val finalFile = File("$destinationPath/update.apk")
+ emit(DownloadProgress.Success(path = finalFile.absolutePath))
} catch (e: Exception) {
emit(DownloadProgress.Error(e))
}
+ }.flowOn(dispatcherProvider.io)
+
+ override suspend fun clean(): Unit = withContext(dispatcherProvider.io) {
+ File(destinationPath).deleteRecursively()
}
}
-
diff --git a/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateDownloader.kt b/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateDownloader.kt
index 194b169..8096827 100644
--- a/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateDownloader.kt
+++ b/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateDownloader.kt
@@ -4,6 +4,8 @@ import kotlinx.coroutines.flow.Flow
interface UpdateDownloader {
fun download(url: String): Flow
+
+ suspend fun clean()
}
sealed interface DownloadProgress {
diff --git a/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateInteractor.kt b/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateInteractor.kt
index 1bd0266..354fb33 100644
--- a/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateInteractor.kt
+++ b/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateInteractor.kt
@@ -31,6 +31,7 @@ class UpdateInteractor(
} else {
emit(
UpdateState.UpdateAvailable(
+ url = checkNotNull(updateInfo.url) { "url required" },
versionName = updateInfo.versionName ?: "v$serverVersion",
changeLog = updateInfo.changelog,
isForceUpdate = updateInfo.force
@@ -51,7 +52,7 @@ class UpdateInteractor(
emit(UpdateState.InstallPermissionRequired)
return@flow
}
-
+ emit(UpdateState.Downloading(0f, 0, 0))
downloader.download(url).collect { progress ->
when (progress) {
is DownloadProgress.Active -> {
@@ -81,6 +82,10 @@ class UpdateInteractor(
installer?.install(path)
}
+ suspend fun reset() {
+ downloader?.clean()
+ }
+
suspend fun hasInstallPermission(): Boolean {
return installer?.hasInstallPermission() ?: false
}
diff --git a/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateState.kt b/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateState.kt
index b9570c6..049035e 100644
--- a/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateState.kt
+++ b/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/domain/UpdateState.kt
@@ -10,6 +10,7 @@ sealed interface UpdateState {
data object Checking : UpdateState
data class UpdateAvailable(
+ val url: String,
val versionName: String,
val changeLog: String?,
val isForceUpdate: Boolean
diff --git a/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateBottomSheet.kt b/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateBottomSheet.kt
index 9550ec9..0a8f526 100644
--- a/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateBottomSheet.kt
+++ b/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateBottomSheet.kt
@@ -16,8 +16,8 @@ import androiddev.libs.updater.updater_client.generated.resources.updater_title_
import androiddev.libs.updater.updater_client.generated.resources.updater_title_not_found
import androiddev.libs.updater.updater_client.generated.resources.updater_title_permission
import androiddev.libs.updater.updater_client.generated.resources.updater_title_ready
-import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -61,17 +61,13 @@ fun UpdateBottomSheet(
sheetState = sheetState,
modifier = modifier
) {
- AnimatedContent(
- targetState = state,
- label = "UpdateStateAnimation",
- modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)
- ) { currentState ->
- when (currentState) {
+ Box(Modifier.padding(horizontal = 8.dp)) {
+ when (val currentState = state) {
is UpdateState.Checking -> UpdateCheckingContent()
is UpdateState.UpdateAvailable -> UpdateAvailableContent(
versionName = currentState.versionName,
changeLog = currentState.changeLog,
- onDownloadClick = { component.downloadUpdate(currentState.versionName) }
+ onDownloadClick = { component.downloadUpdate(currentState.url) }
)
is UpdateState.InstallPermissionRequired -> UpdatePermissionContent(
onGrantClick = { component.requestPermission() }
@@ -89,7 +85,7 @@ fun UpdateBottomSheet(
onRetryClick = { component.checkUpdates() }
)
is UpdateState.UpToDate -> {
- UpdateNotFoundContent(onSkip = { component.reset() })
+ UpdateNotFoundContent(onSkip = { component.reset() })
}
else -> Unit
}
diff --git a/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateComponent.kt b/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateComponent.kt
index 83b6001..c51e48d 100644
--- a/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateComponent.kt
+++ b/libs/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateComponent.kt
@@ -67,7 +67,11 @@ class UpdateComponent(
}
fun reset() {
- _state.value = UpdateState.Idle
+ scope.launch {
+ _state.value = UpdateState.Checking
+ interactor.reset()
+ _state.value = UpdateState.Idle
+ }
}
}