Replace RxJava in extension installer (#9556)

* Replace RxJava in extension installer

Replace common downloadsRelay with Map of individual StateFlows

* Drop RxRelay dependency

* Simplify updateAllExtensions

* Simplify addDownloadState/removeDownloadState

Use immutable Map functions instead of converting to MutableMap
This commit is contained in:
Two-Ai 2023-05-30 10:25:20 -04:00 committed by GitHub
parent 4c65c2311e
commit 0ac38297f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 80 additions and 77 deletions

View file

@ -14,10 +14,11 @@ import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import logcat.LogPriority
import rx.Observable
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
@ -200,7 +201,7 @@ class ExtensionManager(
*
* @param extension The extension to be installed.
*/
fun installExtension(extension: Extension.Available): Observable<InstallStep> {
fun installExtension(extension: Extension.Available): Flow<InstallStep> {
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
}
@ -211,9 +212,9 @@ class ExtensionManager(
*
* @param extension The extension to be updated.
*/
fun updateExtension(extension: Extension.Installed): Observable<InstallStep> {
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == extension.pkgName }
?: return Observable.empty()
?: return emptyFlow()
return installExtension(availableExt)
}

View file

@ -10,20 +10,27 @@ import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.installer.Installer
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.storage.getUriCompat
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.transformWhile
import logcat.LogPriority
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds
/**
* The installer which installs, updates and uninstalls the extensions.
@ -48,10 +55,7 @@ internal class ExtensionInstaller(private val context: Context) {
*/
private val activeDownloads = hashMapOf<String, Long>()
/**
* Relay used to notify the installation step of every download.
*/
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
private val downloadsStateFlows = hashMapOf<Long, MutableStateFlow<InstallStep>>()
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
@ -62,7 +66,7 @@ internal class ExtensionInstaller(private val context: Context) {
* @param url The url of the apk.
* @param extension The extension to install.
*/
fun downloadAndInstall(url: String, extension: Extension) = Observable.defer {
fun downloadAndInstall(url: String, extension: Extension): Flow<InstallStep> {
val pkgName = extension.pkgName
val oldDownload = activeDownloads[pkgName]
@ -83,48 +87,59 @@ internal class ExtensionInstaller(private val context: Context) {
val id = downloadManager.enqueue(request)
activeDownloads[pkgName] = id
downloadsRelay.filter { it.first == id }
.map { it.second }
val downloadStateFlow = MutableStateFlow(InstallStep.Pending)
downloadsStateFlows[id] = downloadStateFlow
// Poll download status
.mergeWith(pollStatus(id))
val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus ->
// Map to our model
when (downloadStatus) {
DownloadManager.STATUS_PENDING -> InstallStep.Pending
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
else -> null
}
}
return merge(downloadStateFlow, pollStatusFlow).transformWhile {
emit(it)
// Stop when the application is installed or errors
.takeUntil { it.isCompleted() }
!it.isCompleted()
}.onCompletion {
// Always notify on main thread
.observeOn(AndroidSchedulers.mainThread())
withUIContext {
// Always remove the download when unsubscribed
.doOnUnsubscribe { deleteDownload(pkgName) }
deleteDownload(pkgName)
}
}
}
/**
* Returns an observable that polls the given download id for its status every second, as the
* Returns a flow that polls the given download id for its status every second, as the
* manager doesn't have any notification system. It'll stop once the download finishes.
*
* @param id The id of the download to poll.
*/
private fun pollStatus(id: Long): Observable<InstallStep> {
private fun downloadStatusFlow(id: Long): Flow<Int> = flow {
val query = DownloadManager.Query().setFilterById(id)
return Observable.interval(0, 1, TimeUnit.SECONDS)
while (true) {
// Get the current download status
.map {
downloadManager.query(query).use { cursor ->
cursor.moveToFirst()
val downloadStatus = downloadManager.query(query).use { cursor ->
if (!cursor.moveToFirst()) return@flow
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
}
emit(downloadStatus)
// Stop polling when the download fails or finishes
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
return@flow
}
delay(1.seconds)
}
}
// Ignore duplicate results
.distinctUntilChanged()
// Stop polling when the download fails or finishes
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
// Map to our model
.flatMap { status ->
when (status) {
DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading)
else -> Observable.empty()
}
}
}
/**
* Starts an intent to install the extension at the given uri.
@ -176,7 +191,7 @@ internal class ExtensionInstaller(private val context: Context) {
* @param step New install step.
*/
fun updateInstallStep(downloadId: Long, step: InstallStep) {
downloadsRelay.call(downloadId to step)
downloadsStateFlows[downloadId]?.let { it.value = step }
}
/**
@ -188,6 +203,7 @@ internal class ExtensionInstaller(private val context: Context) {
val downloadId = activeDownloads.remove(pkgName)
if (downloadId != null) {
downloadManager.remove(downloadId)
downloadsStateFlows.remove(downloadId)
}
if (activeDownloads.isEmpty()) {
downloadReceiver.unregister()
@ -240,7 +256,7 @@ internal class ExtensionInstaller(private val context: Context) {
// Set next installation step
if (uri == null) {
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
downloadsRelay.call(id to InstallStep.Error)
updateInstallStep(id, InstallStep.Error)
return
}

View file

@ -14,16 +14,18 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import rx.Observable
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -130,28 +132,24 @@ class ExtensionsScreenModel(
fun updateAllExtensions() {
coroutineScope.launchIO {
with(state.value) {
if (isEmpty) return@launchIO
items.values
.flatten()
.mapNotNull {
when {
it.extension !is Extension.Installed -> null
!it.extension.hasUpdate -> null
else -> it.extension
}
}
state.value.items.values.flatten()
.map { it.extension }
.filterIsInstance<Extension.Installed>()
.filter { it.hasUpdate }
.forEach(::updateExtension)
}
}
}
fun installExtension(extension: Extension.Available) {
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
coroutineScope.launchIO {
extensionManager.installExtension(extension).collectToInstallUpdate(extension)
}
}
fun updateExtension(extension: Extension.Installed) {
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
coroutineScope.launchIO {
extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
}
}
fun cancelInstallUpdateExtension(extension: Extension) {
@ -159,29 +157,18 @@ class ExtensionsScreenModel(
}
private fun removeDownloadState(extension: Extension) {
_currentDownloads.update { _map ->
val map = _map.toMutableMap()
map.remove(extension.pkgName)
map
}
_currentDownloads.update { it - extension.pkgName }
}
private fun addDownloadState(extension: Extension, installStep: InstallStep) {
_currentDownloads.update { _map ->
val map = _map.toMutableMap()
map[extension.pkgName] = installStep
map
}
_currentDownloads.update { it + Pair(extension.pkgName, installStep) }
}
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: Extension) {
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: Extension) =
this
.doOnUnsubscribe { removeDownloadState(extension) }
.subscribe(
{ installStep -> addDownloadState(extension, installStep) },
{ removeDownloadState(extension) },
)
}
.onEach { installStep -> addDownloadState(extension, installStep) }
.onCompletion { removeDownloadState(extension) }
.collect()
fun uninstallExtension(pkgName: String) {
extensionManager.uninstallExtension(pkgName)

View file

@ -16,7 +16,6 @@ google-services-gradle = "com.google.gms:google-services:4.3.15"
rxandroid = "io.reactivex:rxandroid:1.2.1"
rxjava = "io.reactivex:rxjava:1.3.8"
rxrelay = "com.jakewharton.rxrelay:rxrelay:1.2.0"
flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
@ -92,7 +91,7 @@ voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", vers
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
[bundles]
reactivex = ["rxandroid", "rxjava", "rxrelay"]
reactivex = ["rxandroid", "rxjava"]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
js-engine = ["quickjs-android"]
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]