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 ru.shadowsparky.updater.domain.UpdateInfo
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Factory
|
@Factory(binds = [BackendUpdateFetcher::class])
|
||||||
class BackendUpdateFetcher(
|
class BackendUpdateFetcher(
|
||||||
private val envFetcher: EnvFetcher,
|
private val envFetcher: EnvFetcher,
|
||||||
private val json: Json,
|
private val json: Json,
|
||||||
private val dispatcherProvider: DispatcherProvider
|
private val dispatcherProvider: DispatcherProvider
|
||||||
) : UpdateFetcher {
|
) : UpdateFetcher {
|
||||||
private var config: UpdateInfo? = null
|
private var config: UpdateInfo? = null
|
||||||
|
private var updateFile: File? = null
|
||||||
|
|
||||||
override suspend fun fetch(versionCode: Long): UpdateInfo = withContext(dispatcherProvider.io) {
|
override suspend fun fetch(versionCode: Long): UpdateInfo = withContext(dispatcherProvider.io) {
|
||||||
config?.let { return@withContext it }
|
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 savedMovieFactory: SavedMovieRepositoryFactory,
|
||||||
val movieTagFactory: MovieTagRepositoryFactory,
|
val movieTagFactory: MovieTagRepositoryFactory,
|
||||||
val userTagFactory: UserTagRepositoryFactory,
|
val userTagFactory: UserTagRepositoryFactory,
|
||||||
val updateFetcher: UpdateFetcher
|
val updateFetcher: BackendUpdateFetcher
|
||||||
)
|
)
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
|
|||||||
+6
-1
@@ -1,15 +1,20 @@
|
|||||||
package ru.shadowsparky.vbox.backend.presentation.routing
|
package ru.shadowsparky.vbox.backend.presentation.routing
|
||||||
|
|
||||||
import io.ktor.server.response.respond
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.response.respondFile
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.server.routing.Route
|
||||||
import io.ktor.server.routing.get
|
import io.ktor.server.routing.get
|
||||||
import io.ktor.server.util.getOrFail
|
import io.ktor.server.util.getOrFail
|
||||||
import ru.shadowsparky.updater.domain.UpdateFetcher
|
import ru.shadowsparky.updater.domain.UpdateFetcher
|
||||||
|
import ru.shadowsparky.vbox.backend.data.BackendUpdateFetcher
|
||||||
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
|
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
|
||||||
|
|
||||||
fun Route.setupUpdates(updateFetcher: UpdateFetcher) {
|
fun Route.setupUpdates(updateFetcher: BackendUpdateFetcher) {
|
||||||
get("$DYNAMIC_PREFIX/checkUpdates") {
|
get("$DYNAMIC_PREFIX/checkUpdates") {
|
||||||
val versionCode = call.parameters.getOrFail(UpdateFetcher.ARG_VERSION_CODE).toLong()
|
val versionCode = call.parameters.getOrFail(UpdateFetcher.ARG_VERSION_CODE).toLong()
|
||||||
call.respond(updateFetcher.fetch(versionCode))
|
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">
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"
|
||||||
tools:ignore="RequestInstallPackagesPolicy" />
|
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>
|
</manifest>
|
||||||
|
|||||||
+13
-7
@@ -2,14 +2,17 @@ package ru.shadowsparky.updater.data
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import io.ktor.client.HttpClient
|
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.client.statement.bodyAsChannel
|
||||||
import io.ktor.http.contentLength
|
import io.ktor.http.contentLength
|
||||||
import io.ktor.utils.io.ByteReadChannel
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
import io.ktor.utils.io.readAvailable
|
import io.ktor.utils.io.readAvailable
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
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 org.koin.core.annotation.Factory
|
||||||
|
import ru.shadowsparky.domain.DispatcherProvider
|
||||||
import ru.shadowsparky.updater.domain.DownloadProgress
|
import ru.shadowsparky.updater.domain.DownloadProgress
|
||||||
import ru.shadowsparky.updater.domain.UpdateDownloader
|
import ru.shadowsparky.updater.domain.UpdateDownloader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -18,20 +21,19 @@ import java.io.File
|
|||||||
class KtorUpdateDownloader(
|
class KtorUpdateDownloader(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
app: Application,
|
app: Application,
|
||||||
|
private val dispatcherProvider: DispatcherProvider
|
||||||
) : UpdateDownloader {
|
) : UpdateDownloader {
|
||||||
private val destinationPath = "${app.cacheDir.absolutePath}/update"
|
private val destinationPath = "${app.cacheDir.absolutePath}/update"
|
||||||
|
|
||||||
override fun download(url: String): Flow<DownloadProgress> = flow {
|
override fun download(url: String): Flow<DownloadProgress> = flow {
|
||||||
try {
|
try {
|
||||||
val response = httpClient.get(url)
|
httpClient.prepareGet(url).execute { response ->
|
||||||
val contentLength = response.contentLength() ?: -1L
|
val contentLength = response.contentLength() ?: -1L
|
||||||
val channel: ByteReadChannel = response.bodyAsChannel()
|
val channel: ByteReadChannel = response.bodyAsChannel()
|
||||||
|
|
||||||
val directory = File(destinationPath)
|
val directory = File(destinationPath)
|
||||||
if (!directory.exists()) {
|
if (!directory.exists()) {
|
||||||
directory.mkdirs()
|
directory.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
val fileName = "update.apk"
|
val fileName = "update.apk"
|
||||||
val fullPath = "$destinationPath/$fileName"
|
val fullPath = "$destinationPath/$fileName"
|
||||||
val file = File(fullPath)
|
val file = File(fullPath)
|
||||||
@@ -62,11 +64,15 @@ class KtorUpdateDownloader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
emit(DownloadProgress.Success(path = file.absolutePath))
|
val finalFile = File("$destinationPath/update.apk")
|
||||||
|
emit(DownloadProgress.Success(path = finalFile.absolutePath))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emit(DownloadProgress.Error(e))
|
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 {
|
interface UpdateDownloader {
|
||||||
fun download(url: String): Flow<DownloadProgress>
|
fun download(url: String): Flow<DownloadProgress>
|
||||||
|
|
||||||
|
suspend fun clean()
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface DownloadProgress {
|
sealed interface DownloadProgress {
|
||||||
|
|||||||
+6
-1
@@ -31,6 +31,7 @@ class UpdateInteractor(
|
|||||||
} else {
|
} else {
|
||||||
emit(
|
emit(
|
||||||
UpdateState.UpdateAvailable(
|
UpdateState.UpdateAvailable(
|
||||||
|
url = checkNotNull(updateInfo.url) { "url required" },
|
||||||
versionName = updateInfo.versionName ?: "v$serverVersion",
|
versionName = updateInfo.versionName ?: "v$serverVersion",
|
||||||
changeLog = updateInfo.changelog,
|
changeLog = updateInfo.changelog,
|
||||||
isForceUpdate = updateInfo.force
|
isForceUpdate = updateInfo.force
|
||||||
@@ -51,7 +52,7 @@ class UpdateInteractor(
|
|||||||
emit(UpdateState.InstallPermissionRequired)
|
emit(UpdateState.InstallPermissionRequired)
|
||||||
return@flow
|
return@flow
|
||||||
}
|
}
|
||||||
|
emit(UpdateState.Downloading(0f, 0, 0))
|
||||||
downloader.download(url).collect { progress ->
|
downloader.download(url).collect { progress ->
|
||||||
when (progress) {
|
when (progress) {
|
||||||
is DownloadProgress.Active -> {
|
is DownloadProgress.Active -> {
|
||||||
@@ -81,6 +82,10 @@ class UpdateInteractor(
|
|||||||
installer?.install(path)
|
installer?.install(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun reset() {
|
||||||
|
downloader?.clean()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun hasInstallPermission(): Boolean {
|
suspend fun hasInstallPermission(): Boolean {
|
||||||
return installer?.hasInstallPermission() ?: false
|
return installer?.hasInstallPermission() ?: false
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -10,6 +10,7 @@ sealed interface UpdateState {
|
|||||||
data object Checking : UpdateState
|
data object Checking : UpdateState
|
||||||
|
|
||||||
data class UpdateAvailable(
|
data class UpdateAvailable(
|
||||||
|
val url: String,
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
val changeLog: String?,
|
val changeLog: String?,
|
||||||
val isForceUpdate: Boolean
|
val isForceUpdate: Boolean
|
||||||
|
|||||||
+4
-8
@@ -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_not_found
|
||||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_permission
|
import androiddev.libs.updater.updater_client.generated.resources.updater_title_permission
|
||||||
import androiddev.libs.updater.updater_client.generated.resources.updater_title_ready
|
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.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -61,17 +61,13 @@ fun UpdateBottomSheet(
|
|||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
AnimatedContent(
|
Box(Modifier.padding(horizontal = 8.dp)) {
|
||||||
targetState = state,
|
when (val currentState = state) {
|
||||||
label = "UpdateStateAnimation",
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)
|
|
||||||
) { currentState ->
|
|
||||||
when (currentState) {
|
|
||||||
is UpdateState.Checking -> UpdateCheckingContent()
|
is UpdateState.Checking -> UpdateCheckingContent()
|
||||||
is UpdateState.UpdateAvailable -> UpdateAvailableContent(
|
is UpdateState.UpdateAvailable -> UpdateAvailableContent(
|
||||||
versionName = currentState.versionName,
|
versionName = currentState.versionName,
|
||||||
changeLog = currentState.changeLog,
|
changeLog = currentState.changeLog,
|
||||||
onDownloadClick = { component.downloadUpdate(currentState.versionName) }
|
onDownloadClick = { component.downloadUpdate(currentState.url) }
|
||||||
)
|
)
|
||||||
is UpdateState.InstallPermissionRequired -> UpdatePermissionContent(
|
is UpdateState.InstallPermissionRequired -> UpdatePermissionContent(
|
||||||
onGrantClick = { component.requestPermission() }
|
onGrantClick = { component.requestPermission() }
|
||||||
|
|||||||
+4
@@ -67,8 +67,12 @@ class UpdateComponent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
|
scope.launch {
|
||||||
|
_state.value = UpdateState.Checking
|
||||||
|
interactor.reset()
|
||||||
_state.value = UpdateState.Idle
|
_state.value = UpdateState.Idle
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Factory
|
@Factory
|
||||||
|
|||||||
Reference in New Issue
Block a user