add update file
This commit is contained in:
+11
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class RoutingEntryPoint(
|
||||
val savedMovieFactory: SavedMovieRepositoryFactory,
|
||||
val movieTagFactory: MovieTagRepositoryFactory,
|
||||
val userTagFactory: UserTagRepositoryFactory,
|
||||
val updateFetcher: UpdateFetcher
|
||||
val updateFetcher: BackendUpdateFetcher
|
||||
)
|
||||
|
||||
@Single
|
||||
|
||||
+6
-1
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,17 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"
|
||||
tools:ignore="RequestInstallPackagesPolicy" />
|
||||
|
||||
<application>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
+40
-34
@@ -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<DownloadProgress> = 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -4,6 +4,8 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface UpdateDownloader {
|
||||
fun download(url: String): Flow<DownloadProgress>
|
||||
|
||||
suspend fun clean()
|
||||
}
|
||||
|
||||
sealed interface DownloadProgress {
|
||||
|
||||
+6
-1
@@ -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
|
||||
}
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+5
-9
@@ -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
|
||||
}
|
||||
|
||||
+5
-1
@@ -67,7 +67,11 @@ class UpdateComponent(
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
_state.value = UpdateState.Idle
|
||||
scope.launch {
|
||||
_state.value = UpdateState.Checking
|
||||
interactor.reset()
|
||||
_state.value = UpdateState.Idle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user