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 + } } }