updater ui, data implementation
This commit is contained in:
@@ -15,6 +15,11 @@ kotlin {
|
||||
implementation(libs.ui.tooling.preview)
|
||||
}
|
||||
}
|
||||
val androidMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.androidx.core)
|
||||
}
|
||||
}
|
||||
}
|
||||
android { namespace = "ru.shadowsparky.updater" }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"
|
||||
tools:ignore="RequestInstallPackagesPolicy" />
|
||||
</manifest>
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package ru.shadowsparky.updater.data
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.updater.domain.UpdateInstaller
|
||||
import java.io.File
|
||||
|
||||
@Factory
|
||||
class AndroidUpdateInstaller(private val app: Application) : UpdateInstaller {
|
||||
|
||||
override suspend fun hasInstallPermission(): Boolean {
|
||||
return app.packageManager.canRequestPackageInstalls()
|
||||
}
|
||||
|
||||
override suspend fun requestInstallPermission() {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||
data = "package:${app.packageName}".toUri()
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
app.startActivity(intent)
|
||||
}
|
||||
|
||||
override suspend fun install(path: String) {
|
||||
val file = File(path)
|
||||
if (!file.exists()) return
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
val uri: Uri = FileProvider.getUriForFile(
|
||||
app,
|
||||
"${app.packageName}.provider",
|
||||
file
|
||||
)
|
||||
intent.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
app.startActivity(intent)
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package ru.shadowsparky.updater.data
|
||||
|
||||
import android.app.Application
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
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 org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.updater.domain.DownloadProgress
|
||||
import ru.shadowsparky.updater.domain.UpdateDownloader
|
||||
import java.io.File
|
||||
|
||||
@Factory
|
||||
class KtorUpdateDownloader(
|
||||
private val httpClient: HttpClient,
|
||||
app: Application,
|
||||
) : 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()
|
||||
|
||||
val directory = File(destinationPath)
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
val fileName = "update.apk"
|
||||
val fullPath = "$destinationPath/$fileName"
|
||||
val file = File(fullPath)
|
||||
|
||||
if (file.exists()) file.delete()
|
||||
|
||||
val bufferSize = 8192
|
||||
val buffer = ByteArray(bufferSize)
|
||||
var downloadedBytes = 0L
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(DownloadProgress.Success(path = file.absolutePath))
|
||||
} catch (e: Exception) {
|
||||
emit(DownloadProgress.Error(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="updater_isolated_cache" path="update/" />
|
||||
</paths>
|
||||
@@ -12,6 +12,9 @@ class UpdateInteractor(
|
||||
private val installer: UpdateInstaller?,
|
||||
private val appConfig: AppConfig?
|
||||
) {
|
||||
val hasSupport = fetcher != null && downloader != null && installer != null &&
|
||||
appConfig?.versionCode != null
|
||||
|
||||
fun checkUpdate(): Flow<UpdateState> = flow {
|
||||
if (appConfig?.versionCode == null || fetcher == null) {
|
||||
emit(UpdateState.UpToDate)
|
||||
|
||||
+6
-6
@@ -90,7 +90,7 @@ fun UpdateBottomSheet(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateCheckingContent(modifier: Modifier = Modifier) {
|
||||
private fun UpdateCheckingContent(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
@@ -98,7 +98,7 @@ fun UpdateCheckingContent(modifier: Modifier = Modifier) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateAvailableContent(
|
||||
private fun UpdateAvailableContent(
|
||||
versionName: String,
|
||||
changeLog: String?,
|
||||
onDownloadClick: () -> Unit,
|
||||
@@ -131,7 +131,7 @@ fun UpdateAvailableContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdatePermissionContent(
|
||||
private fun UpdatePermissionContent(
|
||||
onGrantClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -149,7 +149,7 @@ fun UpdatePermissionContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateDownloadingContent(
|
||||
private fun UpdateDownloadingContent(
|
||||
progress: Float,
|
||||
downloadedBytes: Long,
|
||||
totalBytes: Long,
|
||||
@@ -174,7 +174,7 @@ fun UpdateDownloadingContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateReadyContent(
|
||||
private fun UpdateReadyContent(
|
||||
onInstallClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -191,7 +191,7 @@ fun UpdateReadyContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateErrorContent(
|
||||
private fun UpdateErrorContent(
|
||||
message: String,
|
||||
onRetryClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
|
||||
+10
-1
@@ -10,13 +10,15 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.updater.domain.UpdateInteractor
|
||||
import ru.shadowsparky.updater.domain.UpdateState
|
||||
|
||||
class UpdateComponent(
|
||||
componentContext: ComponentContext,
|
||||
private val interactor: UpdateInteractor,
|
||||
private val interactor: UpdateInteractor
|
||||
) : ComponentContext by componentContext {
|
||||
val hasSupport = interactor.hasSupport
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
@@ -64,3 +66,10 @@ class UpdateComponent(
|
||||
scope.launch { interactor.installApk(path) }
|
||||
}
|
||||
}
|
||||
|
||||
@Factory
|
||||
class UpdateComponentFactory(private val interactor: UpdateInteractor) {
|
||||
fun create(componentContext: ComponentContext): UpdateComponent {
|
||||
return UpdateComponent(componentContext, interactor)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user