feat: async UI overhaul
Fixing lag on HomeScreen in the process.
This commit is contained in:
parent
8fdcab8aba
commit
69e82dedc5
17 changed files with 214 additions and 172 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
package com.aliucord.manager
|
package com.aliucord.manager
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.annotation.DelicateCoilApi
|
||||||
import com.aliucord.manager.di.*
|
import com.aliucord.manager.di.*
|
||||||
import com.aliucord.manager.installers.pm.PMInstaller
|
import com.aliucord.manager.installers.pm.PMInstaller
|
||||||
import com.aliucord.manager.manager.*
|
import com.aliucord.manager.manager.*
|
||||||
|
|
@ -17,6 +20,7 @@ import com.aliucord.manager.ui.screens.patchopts.PatchOptionsModel
|
||||||
import com.aliucord.manager.ui.screens.plugins.PluginsModel
|
import com.aliucord.manager.ui.screens.plugins.PluginsModel
|
||||||
import com.aliucord.manager.ui.screens.settings.SettingsModel
|
import com.aliucord.manager.ui.screens.settings.SettingsModel
|
||||||
import com.aliucord.manager.ui.widgets.updater.UpdaterViewModel
|
import com.aliucord.manager.ui.widgets.updater.UpdaterViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import org.koin.core.module.dsl.*
|
import org.koin.core.module.dsl.*
|
||||||
|
|
@ -77,5 +81,13 @@ class ManagerApplication : Application() {
|
||||||
singleOf(::PMInstaller)
|
singleOf(::PMInstaller)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Limit parallel fetching of images using Coil
|
||||||
|
@OptIn(DelicateCoilApi::class)
|
||||||
|
SingletonImageLoader.setUnsafe { context ->
|
||||||
|
ImageLoader.Builder(context)
|
||||||
|
.fetcherCoroutineContext(Dispatchers.IO.limitedParallelism(5))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,17 @@ import io.ktor.client.request.HttpRequestBuilder
|
||||||
import io.ktor.client.request.request
|
import io.ktor.client.request.request
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.isSuccess
|
import io.ktor.http.isSuccess
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class HttpService(
|
class HttpService(
|
||||||
val json: Json,
|
val json: Json,
|
||||||
val http: HttpClient,
|
val http: HttpClient,
|
||||||
) {
|
) {
|
||||||
suspend inline fun <reified T> request(builder: HttpRequestBuilder.() -> Unit = {}): ApiResponse<T> {
|
suspend inline fun <reified T> request(
|
||||||
|
crossinline builder: HttpRequestBuilder.() -> Unit = {},
|
||||||
|
): ApiResponse<T> = withContext(Dispatchers.IO) request@{
|
||||||
var body: String? = null
|
var body: String? = null
|
||||||
|
|
||||||
val response = try {
|
val response = try {
|
||||||
|
|
@ -24,7 +28,7 @@ class HttpService(
|
||||||
body = response.bodyAsText()
|
body = response.bodyAsText()
|
||||||
|
|
||||||
if (T::class == String::class) {
|
if (T::class == String::class) {
|
||||||
return ApiResponse.Success(body as T)
|
return@request ApiResponse.Success(body as T)
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiResponse.Success(json.decodeFromString<T>(body))
|
ApiResponse.Success(json.decodeFromString<T>(body))
|
||||||
|
|
@ -42,6 +46,6 @@ class HttpService(
|
||||||
Log.e(BuildConfig.TAG, "Failed to fetch: error: $t, body: $body")
|
Log.e(BuildConfig.TAG, "Failed to fetch: error: $t, body: $body")
|
||||||
ApiResponse.Failure(ApiFailure(t, body))
|
ApiResponse.Failure(ApiFailure(t, body))
|
||||||
}
|
}
|
||||||
return response
|
return@request response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import com.aliucord.manager.network.models.Contributor
|
||||||
import com.aliucord.manager.network.services.HttpService
|
import com.aliucord.manager.network.services.HttpService
|
||||||
import com.aliucord.manager.network.utils.fold
|
import com.aliucord.manager.network.utils.fold
|
||||||
import com.aliucord.manager.ui.util.toUnsafeImmutable
|
import com.aliucord.manager.ui.util.toUnsafeImmutable
|
||||||
import com.aliucord.manager.util.launchBlock
|
import com.aliucord.manager.util.launchIO
|
||||||
import io.ktor.client.request.url
|
import io.ktor.client.request.url
|
||||||
|
|
||||||
class AboutModel(
|
class AboutModel(
|
||||||
|
|
@ -16,7 +16,7 @@ class AboutModel(
|
||||||
fetchContributors()
|
fetchContributors()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchContributors() = screenModelScope.launchBlock {
|
fun fetchContributors() = screenModelScope.launchIO {
|
||||||
mutableState.value = AboutScreenState.Loading
|
mutableState.value = AboutScreenState.Loading
|
||||||
|
|
||||||
val response = http.request<List<Contributor>> { url(CONTRIBUTORS_API_URL) }
|
val response = http.request<List<Contributor>> { url(CONTRIBUTORS_API_URL) }
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,9 @@ import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
||||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsScreen
|
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsScreen
|
||||||
import com.aliucord.manager.ui.util.DiscordVersion
|
import com.aliucord.manager.ui.util.DiscordVersion
|
||||||
import com.aliucord.manager.ui.util.toUnsafeImmutable
|
import com.aliucord.manager.ui.util.toUnsafeImmutable
|
||||||
import com.aliucord.manager.util.launchBlock
|
import com.aliucord.manager.util.*
|
||||||
import com.aliucord.manager.util.showToast
|
|
||||||
import com.github.diamondminer88.zip.ZipReader
|
import com.github.diamondminer88.zip.ZipReader
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
|
@ -53,22 +51,31 @@ class HomeModel(
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refresh() = screenModelScope.launchBlock {
|
fun refresh(delay: Boolean = false) = screenModelScope.launchIO {
|
||||||
if (refreshingLock.isLocked) return@launchBlock
|
if (refreshingLock.isLocked) return@launchIO
|
||||||
|
|
||||||
|
if (delay) {
|
||||||
|
delay(250)
|
||||||
|
|
||||||
|
if (refreshingLock.isLocked)
|
||||||
|
return@launchIO
|
||||||
|
}
|
||||||
|
|
||||||
refreshingLock.withLock {
|
refreshingLock.withLock {
|
||||||
val packages = fetchAliucordPackages()
|
val packages = fetchAliucordPackages()
|
||||||
|
|
||||||
val jobs = listOf(
|
val jobs = listOf(
|
||||||
screenModelScope.launch { fetchInstallations(packages) },
|
screenModelScope.launch(Dispatchers.IO) {
|
||||||
screenModelScope.launch {
|
fetchInstallations(packages)
|
||||||
|
},
|
||||||
|
screenModelScope.launch(Dispatchers.IO) {
|
||||||
if (remoteDataJson == null || latestAliuhookVersion == null)
|
if (remoteDataJson == null || latestAliuhookVersion == null)
|
||||||
fetchRemoteData()
|
fetchRemoteData()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
jobs.joinAll()
|
jobs.joinAll()
|
||||||
refreshInstallationsUpToDate(packages)
|
mainThread { refreshInstallationsUpToDate(packages) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,7 +111,7 @@ class HomeModel(
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
metadataFile?.let { json.decodeFromStream<InstallMetadata>(it.inputStream()) }
|
metadataFile?.let { json.decodeFromStream<InstallMetadata>(it.inputStream()) }
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.w(BuildConfig.TAG, "Failed to parse Aliucord install metadata from package ${packageName}", t)
|
Log.w(BuildConfig.TAG, "Failed to parse Aliucord install metadata from package $packageName", t)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,8 +121,11 @@ class HomeModel(
|
||||||
return PatchOptionsScreen(prefilledOptions = patchOptions)
|
return PatchOptionsScreen(prefilledOptions = patchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchInstallations(packages: List<PackageInfo>) {
|
private suspend fun fetchInstallations(packages: List<PackageInfo>) {
|
||||||
installsState = InstallsState.Fetching
|
mainThread {
|
||||||
|
if (installsState !is InstallsState.Fetched)
|
||||||
|
installsState = InstallsState.Fetching
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val packageManager = application.packageManager
|
val packageManager = application.packageManager
|
||||||
|
|
@ -143,19 +153,21 @@ class HomeModel(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
installsState = if (aliucordInstallations.isNotEmpty()) {
|
mainThread {
|
||||||
InstallsState.Fetched(data = aliucordInstallations.toUnsafeImmutable())
|
installsState = if (aliucordInstallations.isNotEmpty()) {
|
||||||
} else {
|
InstallsState.Fetched(data = aliucordInstallations.toUnsafeImmutable())
|
||||||
InstallsState.None
|
} else {
|
||||||
|
InstallsState.None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(BuildConfig.TAG, "Failed to query Aliucord installations", t)
|
Log.e(BuildConfig.TAG, "Failed to query Aliucord installations", t)
|
||||||
installsState = InstallsState.Error
|
mainThread { installsState = InstallsState.Error }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshInstallationsUpToDate(packages: List<PackageInfo>) {
|
private suspend fun refreshInstallationsUpToDate(packages: List<PackageInfo>) {
|
||||||
val installations = (installsState as? InstallsState.Fetched)?.data
|
val installations = mainThread { (installsState as? InstallsState.Fetched)?.data }
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -166,22 +178,22 @@ class HomeModel(
|
||||||
data.copy(isUpToDate = isInstallationUpToDate(packageInfo))
|
data.copy(isUpToDate = isInstallationUpToDate(packageInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
installsState = InstallsState.Fetched(data = newInstallations.toUnsafeImmutable())
|
mainThread { installsState = InstallsState.Fetched(data = newInstallations.toUnsafeImmutable()) }
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(BuildConfig.TAG, "Failed to check installations up-to-date", t)
|
Log.e(BuildConfig.TAG, "Failed to check installations up-to-date", t)
|
||||||
installsState = InstallsState.Error
|
mainThread { installsState = InstallsState.Error }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchRemoteData() {
|
private suspend fun fetchRemoteData() {
|
||||||
listOf(
|
listOf(
|
||||||
screenModelScope.launch {
|
screenModelScope.launch(Dispatchers.IO) {
|
||||||
github.getBuildData().fold(
|
github.getBuildData().fold(
|
||||||
success = { remoteDataJson = it },
|
success = { remoteDataJson = it },
|
||||||
fail = { Log.w(BuildConfig.TAG, "Failed to fetch remote build data", it) },
|
fail = { Log.w(BuildConfig.TAG, "Failed to fetch remote build data", it) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
screenModelScope.launch {
|
screenModelScope.launch(Dispatchers.IO) {
|
||||||
maven.getAliuhookVersion().fold(
|
maven.getAliuhookVersion().fold(
|
||||||
success = { latestAliuhookVersion = it },
|
success = { latestAliuhookVersion = it },
|
||||||
fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest Aliuhook version", it) },
|
fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest Aliuhook version", it) },
|
||||||
|
|
@ -190,7 +202,7 @@ class HomeModel(
|
||||||
).joinAll()
|
).joinAll()
|
||||||
|
|
||||||
if (remoteDataJson == null || latestAliuhookVersion == null) {
|
if (remoteDataJson == null || latestAliuhookVersion == null) {
|
||||||
application.showToast(R.string.home_network_fail)
|
mainThread { application.showToast(R.string.home_network_fail) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,7 @@ import com.aliucord.manager.ui.screens.patchopts.PatchOptionsScreen
|
||||||
import com.aliucord.manager.ui.screens.plugins.PluginsScreen
|
import com.aliucord.manager.ui.screens.plugins.PluginsScreen
|
||||||
import com.aliucord.manager.ui.util.paddings.PaddingValuesSides
|
import com.aliucord.manager.ui.util.paddings.PaddingValuesSides
|
||||||
import com.aliucord.manager.ui.util.paddings.exclude
|
import com.aliucord.manager.ui.util.paddings.exclude
|
||||||
import com.aliucord.manager.util.pushOnce
|
import com.aliucord.manager.util.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
|
@ -52,7 +51,7 @@ class HomeScreen : Screen, Parcelable {
|
||||||
|
|
||||||
// Refresh installations list when the screen changes or activity resumes
|
// Refresh installations list when the screen changes or activity resumes
|
||||||
LifecycleResumeEffect(Unit) {
|
LifecycleResumeEffect(Unit) {
|
||||||
model.refresh()
|
model.refresh(delay = true)
|
||||||
|
|
||||||
onPauseOrDispose {}
|
onPauseOrDispose {}
|
||||||
}
|
}
|
||||||
|
|
@ -66,8 +65,9 @@ class HomeScreen : Screen, Parcelable {
|
||||||
padding = padding,
|
padding = padding,
|
||||||
onClickInstall = { navigator.pushOnce(PatchOptionsScreen()) },
|
onClickInstall = { navigator.pushOnce(PatchOptionsScreen()) },
|
||||||
onUpdate = {
|
onUpdate = {
|
||||||
scope.launch {
|
scope.launchIO {
|
||||||
navigator.push(model.createPrefilledPatchOptsScreen(it))
|
val screen = model.createPrefilledPatchOptsScreen(it)
|
||||||
|
mainThread { navigator.push(screen) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOpenApp = model::openApp,
|
onOpenApp = model::openApp,
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import com.aliucord.manager.BuildConfig
|
import com.aliucord.manager.BuildConfig
|
||||||
import com.aliucord.manager.R
|
import com.aliucord.manager.R
|
||||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptions.IconReplacement
|
import com.aliucord.manager.ui.screens.patchopts.PatchOptions.IconReplacement
|
||||||
import com.aliucord.manager.util.launchBlock
|
import com.aliucord.manager.util.*
|
||||||
import com.aliucord.manager.util.showToast
|
|
||||||
import dev.zt64.compose.pipette.HsvColor
|
import dev.zt64.compose.pipette.HsvColor
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
|
@ -43,7 +42,7 @@ class IconOptionsModel(
|
||||||
// ---------- Replacement color ---------- //
|
// ---------- Replacement color ---------- //
|
||||||
var selectedImage by mutableStateOf<ByteArray?>(null)
|
var selectedImage by mutableStateOf<ByteArray?>(null)
|
||||||
|
|
||||||
fun changeSelectedImageUri(uri: Uri) = screenModelScope.launchBlock {
|
fun changeSelectedImageUri(uri: Uri) = screenModelScope.launchIO {
|
||||||
try {
|
try {
|
||||||
// Check file size first
|
// Check file size first
|
||||||
val query = application.contentResolver.query(uri, null, null, null, null)
|
val query = application.contentResolver.query(uri, null, null, null, null)
|
||||||
|
|
@ -55,8 +54,8 @@ class IconOptionsModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size > 1024 * 256) { // 256KiB
|
if (size > 1024 * 256) { // 256KiB
|
||||||
application.showToast(R.string.iconopts_failed_image_too_big)
|
mainThread { application.showToast(R.string.iconopts_failed_image_too_big) }
|
||||||
return@launchBlock
|
return@launchIO
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read file bytes
|
// Read file bytes
|
||||||
|
|
@ -65,10 +64,10 @@ class IconOptionsModel(
|
||||||
?.use { it.readBytes() }
|
?.use { it.readBytes() }
|
||||||
?: throw IOException("Failed to open input stream")
|
?: throw IOException("Failed to open input stream")
|
||||||
|
|
||||||
selectedImage = bytes
|
mainThread { selectedImage = bytes }
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
application.showToast(R.string.iconopts_failed_image)
|
|
||||||
Log.w(BuildConfig.TAG, "Failed to open selected foreground replacement image", t)
|
Log.w(BuildConfig.TAG, "Failed to open selected foreground replacement image", t)
|
||||||
|
mainThread { application.showToast(R.string.iconopts_failed_image) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ class LogScreenModel(
|
||||||
/**
|
/**
|
||||||
* Formats the log data into a file and writes it to the downloads folder.
|
* Formats the log data into a file and writes it to the downloads folder.
|
||||||
*/
|
*/
|
||||||
fun saveLog() = screenModelScope.launchBlock {
|
fun saveLog() = screenModelScope.launchIO {
|
||||||
val data = data ?: return@launchBlock
|
val data = data ?: return@launchIO
|
||||||
|
|
||||||
val formattedDate = data.getFormattedInstallDate()
|
val formattedDate = data.getFormattedInstallDate()
|
||||||
val content = data.getLogFileContents()
|
val content = data.getLogFileContents()
|
||||||
|
|
@ -43,8 +43,8 @@ class LogScreenModel(
|
||||||
/**
|
/**
|
||||||
* Writes the log to internal cache and launches a share intent of the log file.
|
* Writes the log to internal cache and launches a share intent of the log file.
|
||||||
*/
|
*/
|
||||||
fun shareLog() = screenModelScope.launchBlock {
|
fun shareLog() {
|
||||||
val data = data ?: return@launchBlock
|
val data = data ?: return
|
||||||
val formattedDate = data.getFormattedInstallDate()
|
val formattedDate = data.getFormattedInstallDate()
|
||||||
val formattedName = "Aliucord Install $formattedDate.log"
|
val formattedName = "Aliucord Install $formattedDate.log"
|
||||||
val content = data.getLogFileContents()
|
val content = data.getLogFileContents()
|
||||||
|
|
@ -79,14 +79,16 @@ class LogScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadLogData() = screenModelScope.launchBlock {
|
private fun loadLogData() = screenModelScope.launchIO {
|
||||||
val result = logs.fetchInstallData(installId)
|
val result = logs.fetchInstallData(installId)
|
||||||
|
|
||||||
if (result != null) {
|
mainThread {
|
||||||
data = result
|
if (result != null) {
|
||||||
} else {
|
data = result
|
||||||
shouldCloseScreen = true
|
} else {
|
||||||
application.showToast(R.string.network_load_fail)
|
shouldCloseScreen = true
|
||||||
|
application.showToast(R.string.network_load_fail)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@ import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import com.aliucord.manager.R
|
import com.aliucord.manager.R
|
||||||
import com.aliucord.manager.manager.InstallLogManager
|
import com.aliucord.manager.manager.InstallLogManager
|
||||||
import com.aliucord.manager.util.launchBlock
|
import com.aliucord.manager.util.*
|
||||||
import com.aliucord.manager.util.showToast
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
|
@ -26,18 +25,21 @@ class LogsListScreenModel(
|
||||||
loadLogsList()
|
loadLogsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteLogs() = screenModelScope.launchBlock {
|
fun deleteLogs() = screenModelScope.launchIO {
|
||||||
logsManager.deleteAllEntries()
|
logsManager.deleteAllEntries()
|
||||||
logEntries.clear()
|
|
||||||
application.showToast(R.string.logs_status_delete_success)
|
mainThread {
|
||||||
|
logEntries.clear()
|
||||||
|
application.showToast(R.string.logs_status_delete_success)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadLogsList() = screenModelScope.launchBlock {
|
private fun loadLogsList() = screenModelScope.launchIO {
|
||||||
for (installId in logsManager.fetchInstallDataEntries()) {
|
for (installId in logsManager.fetchInstallDataEntries()) {
|
||||||
val data = logsManager.fetchInstallData(id = installId)
|
val data = logsManager.fetchInstallData(id = installId)
|
||||||
?: continue
|
?: continue
|
||||||
|
|
||||||
logEntries += LogEntry(
|
val entry = LogEntry(
|
||||||
id = data.id,
|
id = data.id,
|
||||||
isError = data.isError,
|
isError = data.isError,
|
||||||
installDate = DateUtils.getRelativeDateTimeString(
|
installDate = DateUtils.getRelativeDateTimeString(
|
||||||
|
|
@ -53,6 +55,8 @@ class LogsListScreenModel(
|
||||||
?.take(3)
|
?.take(3)
|
||||||
?.toImmutableList(),
|
?.toImmutableList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mainThread { logEntries += entry }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,7 @@ import com.aliucord.manager.patcher.steps.install.InstallStep
|
||||||
import com.aliucord.manager.patcher.util.InsufficientStorageException
|
import com.aliucord.manager.patcher.util.InsufficientStorageException
|
||||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
||||||
import com.aliucord.manager.ui.util.toUnsafeImmutable
|
import com.aliucord.manager.ui.util.toUnsafeImmutable
|
||||||
import com.aliucord.manager.util.launchBlock
|
import com.aliucord.manager.util.*
|
||||||
import com.aliucord.manager.util.showToast
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
@ -77,14 +76,14 @@ class PatchingScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearCache() = screenModelScope.launchBlock {
|
fun clearCache() = screenModelScope.launchIO {
|
||||||
paths.clearCache()
|
paths.clearCache()
|
||||||
application.showToast(R.string.action_cleared_cache)
|
mainThread { application.showToast(R.string.action_cleared_cache) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentInstallId(): String? = installId
|
fun getCurrentInstallId(): String? = installId
|
||||||
|
|
||||||
fun cancelInstall() = screenModelScope.launchBlock(Dispatchers.IO) {
|
fun cancelInstall() = screenModelScope.launchIO {
|
||||||
runnerJob?.cancel("Manual cancellation")
|
runnerJob?.cancel("Manual cancellation")
|
||||||
|
|
||||||
val incompleteDownloadStep = stepRunner?.steps
|
val incompleteDownloadStep = stepRunner?.steps
|
||||||
|
|
@ -95,7 +94,7 @@ class PatchingScreenModel(
|
||||||
paths.patchingWorkingDir().deleteRecursively()
|
paths.patchingWorkingDir().deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun install() = screenModelScope.launch {
|
fun install() = screenModelScope.launchBlock {
|
||||||
runnerJob?.cancel("Manual cancellation")
|
runnerJob?.cancel("Manual cancellation")
|
||||||
steps = null
|
steps = null
|
||||||
|
|
||||||
|
|
@ -104,7 +103,7 @@ class PatchingScreenModel(
|
||||||
startTime = Clock.System.now()
|
startTime = Clock.System.now()
|
||||||
mutableState.value = PatchingScreenState.Working
|
mutableState.value = PatchingScreenState.Working
|
||||||
|
|
||||||
runnerJob = screenModelScope.launch {
|
runnerJob = screenModelScope.launch(Dispatchers.Default) {
|
||||||
Log.i(BuildConfig.TAG, "Starting installation with environment:\n" + installLogs.getEnvironmentInfo())
|
Log.i(BuildConfig.TAG, "Starting installation with environment:\n" + installLogs.getEnvironmentInfo())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ import androidx.core.content.getSystemService
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import com.aliucord.manager.manager.PreferencesManager
|
import com.aliucord.manager.manager.PreferencesManager
|
||||||
import com.aliucord.manager.util.debounce
|
import com.aliucord.manager.util.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class PatchOptionsModel(
|
class PatchOptionsModel(
|
||||||
prefilledOptions: PatchOptions,
|
prefilledOptions: PatchOptions,
|
||||||
|
|
@ -119,24 +118,26 @@ class PatchOptionsModel(
|
||||||
private val fetchPkgNameStateDebounced: () -> Unit =
|
private val fetchPkgNameStateDebounced: () -> Unit =
|
||||||
screenModelScope.debounce(100L, function = ::fetchPkgNameState)
|
screenModelScope.debounce(100L, function = ::fetchPkgNameState)
|
||||||
|
|
||||||
private fun fetchPkgNameState() {
|
private suspend fun fetchPkgNameState() {
|
||||||
if (packageName.length !in (3..150) || !PACKAGE_REGEX.matches(this.packageName)) {
|
val state = if (packageName.length !in (3..150) || !PACKAGE_REGEX.matches(this.packageName)) {
|
||||||
packageNameState = PackageNameState.Invalid
|
PackageNameState.Invalid
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
context.packageManager.getPackageInfo(packageName, 0)
|
context.packageManager.getPackageInfo(packageName, 0)
|
||||||
packageNameState = PackageNameState.Taken
|
PackageNameState.Taken
|
||||||
} catch (_: NameNotFoundException) {
|
} catch (_: NameNotFoundException) {
|
||||||
packageNameState = PackageNameState.Ok
|
PackageNameState.Ok
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mainThread { packageNameState = state }
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
screenModelScope.launch { fetchPkgNameState() }
|
|
||||||
|
|
||||||
if (!prefs.showNetworkWarning)
|
if (!prefs.showNetworkWarning)
|
||||||
showNetworkWarningDialog = false
|
showNetworkWarningDialog = false
|
||||||
|
|
||||||
|
screenModelScope.launchBlock { fetchPkgNameState() }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,14 @@ import com.aliucord.manager.ui.screens.plugins.model.PluginItem
|
||||||
import com.aliucord.manager.ui.screens.plugins.model.PluginManifest
|
import com.aliucord.manager.ui.screens.plugins.model.PluginManifest
|
||||||
import com.aliucord.manager.ui.util.emptyImmutableList
|
import com.aliucord.manager.ui.util.emptyImmutableList
|
||||||
import com.aliucord.manager.ui.util.toUnsafeImmutable
|
import com.aliucord.manager.ui.util.toUnsafeImmutable
|
||||||
import com.aliucord.manager.util.launchBlock
|
import com.aliucord.manager.util.*
|
||||||
import com.aliucord.manager.util.showToast
|
|
||||||
import com.github.diamondminer88.zip.ZipReader
|
import com.github.diamondminer88.zip.ZipReader
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -58,7 +57,7 @@ class PluginsModel(
|
||||||
}.toUnsafeImmutable()
|
}.toUnsafeImmutable()
|
||||||
}
|
}
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = screenModelScope,
|
scope = screenModelScope + Dispatchers.Default,
|
||||||
started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO),
|
started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO),
|
||||||
initialValue = plugins.value,
|
initialValue = plugins.value,
|
||||||
)
|
)
|
||||||
|
|
@ -83,36 +82,32 @@ class PluginsModel(
|
||||||
showUninstallDialog = null
|
showUninstallDialog = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uninstallPlugin(pluginPath: String) = screenModelScope.launchBlock(Dispatchers.IO) {
|
fun uninstallPlugin(plugin: PluginItem) = screenModelScope.launchIO {
|
||||||
if (!plugins.value.any { it.path == pluginPath }) {
|
if (!plugins.value.any { it.path == plugin.path }) {
|
||||||
hideUninstallDialog()
|
mainThread { hideUninstallDialog() }
|
||||||
return@launchBlock
|
return@launchIO
|
||||||
}
|
}
|
||||||
|
|
||||||
val deleteSuccess = try {
|
val deleteSuccess = try {
|
||||||
File(pluginPath).delete()
|
File(plugin.path).delete()
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(BuildConfig.TAG, "Failed to delete plugin", t)
|
Log.e(BuildConfig.TAG, "Failed to delete plugin", t)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!deleteSuccess) {
|
if (!deleteSuccess) {
|
||||||
withContext(Dispatchers.Main) {
|
mainThread {
|
||||||
|
hideUninstallDialog()
|
||||||
context.showToast(R.string.plugins_error)
|
context.showToast(R.string.plugins_error)
|
||||||
}
|
}
|
||||||
hideUninstallDialog()
|
return@launchIO
|
||||||
return@launchBlock
|
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
plugins.update { (it - plugin).toUnsafeImmutable() }
|
||||||
plugins.getAndUpdate {
|
mainThread { hideUninstallDialog() }
|
||||||
it.filter { it.path != pluginPath }.toUnsafeImmutable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hideUninstallDialog()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPluginEnabled(pluginName: String, enabled: Boolean) = screenModelScope.launchBlock(Dispatchers.IO) {
|
fun setPluginEnabled(pluginName: String, enabled: Boolean) = screenModelScope.launchIO {
|
||||||
aliucordJsonMutex.withLock {
|
aliucordJsonMutex.withLock {
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
try {
|
try {
|
||||||
|
|
@ -126,11 +121,11 @@ class PluginsModel(
|
||||||
.use { json.encodeToStream(settings, it) }
|
.use { json.encodeToStream(settings, it) }
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(BuildConfig.TAG, "Failed to write Aliucord.json", t)
|
Log.e(BuildConfig.TAG, "Failed to write Aliucord.json", t)
|
||||||
withContext(Dispatchers.Main) { error = true }
|
mainThread { error = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
mainThread {
|
||||||
plugins.value.forEach {
|
plugins.value.forEach {
|
||||||
if (it.manifest.name == pluginName)
|
if (it.manifest.name == pluginName)
|
||||||
it.enabled = enabled
|
it.enabled = enabled
|
||||||
|
|
@ -139,7 +134,7 @@ class PluginsModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by screen to load initial data
|
// Called by screen to load initial data
|
||||||
fun refreshData() = screenModelScope.launchBlock(Dispatchers.IO) {
|
fun refreshData() = screenModelScope.launchIO {
|
||||||
loadPlugins()
|
loadPlugins()
|
||||||
loadPluginsEnabled()
|
loadPluginsEnabled()
|
||||||
}
|
}
|
||||||
|
|
@ -157,11 +152,11 @@ class PluginsModel(
|
||||||
.mapValues { it.value.jsonPrimitive.boolean }
|
.mapValues { it.value.jsonPrimitive.boolean }
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(BuildConfig.TAG, "Failed to load Aliucord.json", t)
|
Log.e(BuildConfig.TAG, "Failed to load Aliucord.json", t)
|
||||||
withContext(Dispatchers.Main) { error = true }
|
mainThread { error = true }
|
||||||
emptyMap()
|
emptyMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
mainThread {
|
||||||
plugins.value.forEach {
|
plugins.value.forEach {
|
||||||
if (!pluginToggles.getOrDefault(it.manifest.name, true))
|
if (!pluginToggles.getOrDefault(it.manifest.name, true))
|
||||||
it.enabled = false
|
it.enabled = false
|
||||||
|
|
@ -185,12 +180,10 @@ class PluginsModel(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
plugins.value = pluginItems.toUnsafeImmutable()
|
||||||
plugins.value = pluginItems.toUnsafeImmutable()
|
|
||||||
}
|
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(BuildConfig.TAG, "Failed to load plugins", t)
|
Log.e(BuildConfig.TAG, "Failed to load plugins", t)
|
||||||
withContext(Dispatchers.Main) { error = true }
|
mainThread { error = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class PluginsScreen : Screen, Parcelable {
|
||||||
model.showUninstallDialog?.let { plugin ->
|
model.showUninstallDialog?.let { plugin ->
|
||||||
UninstallPluginDialog(
|
UninstallPluginDialog(
|
||||||
pluginName = plugin.manifest.name,
|
pluginName = plugin.manifest.name,
|
||||||
onConfirm = { model.uninstallPlugin(plugin.path) },
|
onConfirm = { model.uninstallPlugin(plugin) },
|
||||||
onDismiss = model::hideUninstallDialog
|
onDismiss = model::hideUninstallDialog
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,13 @@ class SettingsModel(
|
||||||
preferences.keepPatchedApks = value
|
preferences.keepPatchedApks = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearCache() = screenModelScope.launchBlock {
|
fun clearCache() = screenModelScope.launchIO {
|
||||||
paths.clearCache()
|
paths.clearCache()
|
||||||
patchedApkExists = false
|
|
||||||
application.showToast(R.string.action_cleared_cache)
|
mainThread {
|
||||||
|
patchedApkExists = false
|
||||||
|
application.showToast(R.string.action_cleared_cache)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyInstallInfo() {
|
fun copyInstallInfo() {
|
||||||
|
|
@ -54,7 +57,7 @@ class SettingsModel(
|
||||||
application.showToast(R.string.action_copied)
|
application.showToast(R.string.action_copied)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shareApk() = screenModelScope.launchBlock {
|
fun shareApk() {
|
||||||
val file = paths.patchingWorkingDir().resolve("patched.apk")
|
val file = paths.patchingWorkingDir().resolve("patched.apk")
|
||||||
val fileUri = FileProvider.getUriForFile(
|
val fileUri = FileProvider.getUriForFile(
|
||||||
/* context = */ application,
|
/* context = */ application,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package com.aliucord.manager.ui.widgets.updater
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -18,6 +18,8 @@ fun UpdaterDialog(
|
||||||
) {
|
) {
|
||||||
if (!viewModel.showDialog) return
|
if (!viewModel.showDialog) return
|
||||||
|
|
||||||
|
val isWorking by viewModel.isWorking.collectAsState(initial = false)
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
|
|
@ -27,7 +29,7 @@ fun UpdaterDialog(
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
if (!viewModel.isWorking) {
|
if (isWorking) {
|
||||||
Text(stringResource(R.string.action_update))
|
Text(stringResource(R.string.action_update))
|
||||||
} else {
|
} else {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
|
|
@ -42,7 +44,7 @@ fun UpdaterDialog(
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = viewModel::dismissDialog,
|
onClick = viewModel::dismissDialog,
|
||||||
enabled = !viewModel.isWorking
|
enabled = isWorking,
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.action_dismiss))
|
Text(stringResource(R.string.action_dismiss))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ import com.aliucord.manager.manager.download.KtorDownloadManager
|
||||||
import com.aliucord.manager.network.services.AliucordGithubService
|
import com.aliucord.manager.network.services.AliucordGithubService
|
||||||
import com.aliucord.manager.network.utils.SemVer
|
import com.aliucord.manager.network.utils.SemVer
|
||||||
import com.aliucord.manager.network.utils.getOrThrow
|
import com.aliucord.manager.network.utils.getOrThrow
|
||||||
import com.aliucord.manager.util.showToast
|
import com.aliucord.manager.util.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class UpdaterViewModel(
|
class UpdaterViewModel(
|
||||||
|
|
@ -31,18 +32,18 @@ class UpdaterViewModel(
|
||||||
private set
|
private set
|
||||||
var targetVersion by mutableStateOf<String?>(null)
|
var targetVersion by mutableStateOf<String?>(null)
|
||||||
private set
|
private set
|
||||||
var isWorking by mutableStateOf(false)
|
val isWorking: Flow<Boolean>
|
||||||
private set
|
private field = MutableStateFlow(false)
|
||||||
|
|
||||||
private var targetApkUrl: String? = null
|
private var targetApkUrl: String? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launchIO {
|
||||||
try {
|
try {
|
||||||
fetchInfo()
|
fetchInfo()
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(BuildConfig.TAG, "Failed to check for updates!", t)
|
Log.e(BuildConfig.TAG, "Failed to check for updates!", t)
|
||||||
application.showToast(R.string.updater_check_fail)
|
mainThread { application.showToast(R.string.updater_check_fail) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,66 +52,66 @@ class UpdaterViewModel(
|
||||||
showDialog = false
|
showDialog = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun triggerUpdate() {
|
fun triggerUpdate() = viewModelScope.launchIO {
|
||||||
if (isWorking) return
|
if (!isWorking.compareAndSet(expect = false, update = true))
|
||||||
val url = targetApkUrl ?: return
|
return@launchIO
|
||||||
|
|
||||||
viewModelScope.launch {
|
val url = targetApkUrl ?: return@launchIO
|
||||||
isWorking = true
|
val apkFile = application.cacheDir.resolve("manager.apk")
|
||||||
|
|
||||||
val apkFile = application.cacheDir.resolve("manager.apk")
|
try {
|
||||||
|
apkFile.apply {
|
||||||
|
parentFile!!.mkdirs()
|
||||||
|
exists() && delete()
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
val installer = installers.getInstaller(InstallerSetting.PM)
|
||||||
apkFile.apply {
|
|
||||||
parentFile!!.mkdirs()
|
when (val downloadResult = downloader.download(url, apkFile)) {
|
||||||
exists() && delete()
|
is IDownloadManager.Result.Success ->
|
||||||
|
Log.d(BuildConfig.TAG, "Downloaded update")
|
||||||
|
|
||||||
|
is IDownloadManager.Result.Cancelled -> {
|
||||||
|
Log.i(BuildConfig.TAG, "Update cancelled")
|
||||||
|
return@launchIO
|
||||||
}
|
}
|
||||||
|
|
||||||
val installer = installers.getInstaller(InstallerSetting.PM)
|
is IDownloadManager.Result.Error ->
|
||||||
|
throw IllegalStateException("Failed to download update: ${downloadResult.getDebugReason()}", downloadResult.getError())
|
||||||
|
}
|
||||||
|
|
||||||
when (val downloadResult = downloader.download(url, apkFile)) {
|
val installResult = installer.waitInstall(
|
||||||
is IDownloadManager.Result.Success ->
|
apks = listOf(apkFile),
|
||||||
Log.d(BuildConfig.TAG, "Downloaded update")
|
silent = true,
|
||||||
|
)
|
||||||
|
|
||||||
is IDownloadManager.Result.Cancelled -> {
|
when (installResult) {
|
||||||
Log.i(BuildConfig.TAG, "Update cancelled")
|
InstallerResult.Success -> {
|
||||||
return@launch
|
Log.w(BuildConfig.TAG, "Update completed without restarting app!")
|
||||||
}
|
exitProcess(1)
|
||||||
|
|
||||||
is IDownloadManager.Result.Error ->
|
|
||||||
throw IllegalStateException("Failed to download update: ${downloadResult.getDebugReason()}", downloadResult.getError())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val installResult = installer.waitInstall(
|
is InstallerResult.Cancelled ->
|
||||||
apks = listOf(apkFile),
|
Log.i(BuildConfig.TAG, "Update cancelled")
|
||||||
silent = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
when (installResult) {
|
is InstallerResult.Error ->
|
||||||
InstallerResult.Success -> {
|
throw Exception("Failed to install update: ${installResult.getDebugReason()}")
|
||||||
Log.w(BuildConfig.TAG, "Update completed without restarting app!")
|
}
|
||||||
exitProcess(1)
|
} catch (t: Throwable) {
|
||||||
}
|
Log.e(BuildConfig.TAG, "Failed to perform update!")
|
||||||
|
t.printStackTrace()
|
||||||
|
|
||||||
is InstallerResult.Cancelled ->
|
mainThread {
|
||||||
Log.i(BuildConfig.TAG, "Update cancelled")
|
|
||||||
|
|
||||||
is InstallerResult.Error ->
|
|
||||||
throw Exception("Failed to install update: ${installResult.getDebugReason()}")
|
|
||||||
}
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.e(BuildConfig.TAG, "Failed to perform update!")
|
|
||||||
t.printStackTrace()
|
|
||||||
application.showToast(R.string.updater_update_fail)
|
application.showToast(R.string.updater_update_fail)
|
||||||
launchReleasesPage()
|
launchReleasesPage()
|
||||||
} finally {
|
}
|
||||||
isWorking = false
|
} finally {
|
||||||
|
isWorking.value = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
apkFile.apply { exists() && delete() }
|
apkFile.apply { exists() && delete() }
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.w(BuildConfig.TAG, "Failed to clean up installed update!", t)
|
Log.w(BuildConfig.TAG, "Failed to clean up installed update!", t)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,10 +152,12 @@ class UpdaterViewModel(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetVersion = version.toString()
|
|
||||||
targetApkUrl = apkUrl
|
|
||||||
showDialog = true
|
|
||||||
Log.d(BuildConfig.TAG, "Found an update! $targetVersion $targetApkUrl")
|
Log.d(BuildConfig.TAG, "Found an update! $targetVersion $targetApkUrl")
|
||||||
|
mainThread {
|
||||||
|
targetVersion = version.toString()
|
||||||
|
targetApkUrl = apkUrl
|
||||||
|
showDialog = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchReleasesPage() {
|
private fun launchReleasesPage() {
|
||||||
|
|
|
||||||
|
|
@ -28,18 +28,18 @@ fun Context.copyToClipboard(text: String) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.saveFile(name: String, text: String): Boolean {
|
suspend fun Context.saveFile(name: String, text: String): Boolean {
|
||||||
val downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
val downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
val file = File(downloads, name)
|
val file = File(downloads, name)
|
||||||
val base = downloads.parentFile!!
|
val base = downloads.parentFile!!
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
file.writeText(text)
|
file.writeText(text)
|
||||||
showToast(R.string.installer_file_save_success, file.toRelativeString(base))
|
mainThread { showToast(R.string.installer_file_save_success, file.toRelativeString(base)) }
|
||||||
true
|
true
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(BuildConfig.TAG, Log.getStackTraceString(e))
|
Log.e(BuildConfig.TAG, Log.getStackTraceString(e))
|
||||||
showToast(R.string.installer_file_save_failed, file.toRelativeString(base))
|
mainThread { showToast(R.string.installer_file_save_failed, file.toRelativeString(base)) }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,25 @@ import kotlinx.coroutines.*
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch a Job for a block without returning anything
|
* Launches a Job in a fixed-size CPU-bound thread pool.
|
||||||
|
* Used for heavy or intensive CPU-bound tasks.
|
||||||
*/
|
*/
|
||||||
inline fun CoroutineScope.launchBlock(
|
inline fun CoroutineScope.launchBlock(
|
||||||
context: CoroutineContext = Dispatchers.Main,
|
context: CoroutineContext = Dispatchers.Default,
|
||||||
noinline block: suspend CoroutineScope.() -> Unit,
|
noinline block: suspend CoroutineScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
launch(context, block = block)
|
launch(context, block = block)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper util to run a block with the main thread context
|
* Launches a Job on a background thread in a dynamically sized thread pool.
|
||||||
|
* Used for IO or other lightweight tasks that spend most of their time waiting.
|
||||||
*/
|
*/
|
||||||
suspend inline fun mainThread(noinline block: CoroutineScope.() -> Unit) =
|
inline fun CoroutineScope.launchIO(noinline block: suspend CoroutineScope.() -> Unit) =
|
||||||
|
launchBlock(Dispatchers.IO, block)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility wrapper around [withContext] to switch to the main thread for the [block].
|
||||||
|
*/
|
||||||
|
suspend inline fun <T> mainThread(noinline block: suspend CoroutineScope.() -> T): T =
|
||||||
withContext(Dispatchers.Main, block)
|
withContext(Dispatchers.Main, block)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue